feat: 支持工作流插件复用与试运行

- 新增工作流插件类型、发布快照同步、实时可用性与下线影响检查

- 收口绑定候选、分类权限、间接环路校验与运行态优雅降级

- 补齐管理端工作流插件配置、详情与试运行界面及定向测试
This commit is contained in:
2026-04-12 13:15:13 +08:00
parent 6da90e2296
commit 47655a728b
57 changed files with 4018 additions and 780 deletions

View File

@@ -11,6 +11,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.entity.WorkflowExecStep;
@@ -62,7 +63,11 @@ public class ChainEventListenerForSave implements ChainEventListener {
log.info("ChainStartEvent: {}", event);
ChainDefinition definition = chain.getDefinition();
ChainState state = chain.getState();
Workflow workflow = workflowService.getById(definition.getId());
Workflow workflow = resolveWorkflow(definition);
if (workflow == null) {
log.error("ChainStartEvent: workflow not found, definitionId={}", definition.getId());
return;
}
String instanceId = state.getInstanceId();
WorkflowExecResult existed = workflowExecResultService.getByExecKey(instanceId);
if (existed != null) {
@@ -176,4 +181,26 @@ public class ChainEventListenerForSave implements ChainEventListener {
ChainState chainState = chain.getChainStateRepository().load(parentInstanceId);
return findAncestorState(chainState, chain);
}
/**
* 根据定义 ID 解析当前执行所对应的工作流。
* 已发布快照执行会使用 published 前缀,需要先还原为真实工作流 ID。
*/
private Workflow resolveWorkflow(ChainDefinition definition) {
if (definition == null || StrUtil.isBlank(definition.getId())) {
return null;
}
String definitionId = definition.getId();
String workflowId = PublishedWorkflowDefinitionIds.unwrap(definitionId);
try {
java.math.BigInteger id = new java.math.BigInteger(workflowId);
if (PublishedWorkflowDefinitionIds.isPublished(definitionId)) {
return workflowService.getPublishedById(id);
}
return workflowService.getById(id);
} catch (NumberFormatException ex) {
log.error("Unsupported workflow definition id: {}", definitionId, ex);
return null;
}
}
}

View File

@@ -9,7 +9,11 @@ import org.springframework.util.StringUtils;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckIssue;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.plugin.workflow.dependency.WorkflowPluginDependencyService;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;
@@ -40,6 +44,7 @@ public class WorkflowCheckService {
private static final String TYPE_END = "endNode";
private static final String TYPE_LOOP = "loopNode";
private static final String TYPE_WORKFLOW = "workflow-node";
private static final String TYPE_PLUGIN = "plugin-node";
@Resource
private WorkflowService workflowService;
@@ -47,6 +52,12 @@ public class WorkflowCheckService {
private ChainParser chainParser;
@Resource
private WorkflowDatacenterContentService workflowDatacenterContentService;
@Resource
private WorkflowPluginDependencyService workflowPluginDependencyService;
@Resource
private PluginItemService pluginItemService;
@Resource
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
public WorkflowCheckResult checkWorkflow(BigInteger workflowId, WorkflowCheckStage stage) {
if (workflowId == null) {
@@ -66,6 +77,9 @@ public class WorkflowCheckService {
List<WorkflowCheckIssue> issues = new ArrayList<>();
Set<String> issueKeys = new LinkedHashSet<>();
ParsedWorkflow parsedWorkflow = parseAndCheckBase(content, issues, issueKeys);
if (parsedWorkflow != null) {
checkPluginSchemaHashes(parsedWorkflow, issues, issueKeys);
}
if (stage == WorkflowCheckStage.PRE_EXECUTE && parsedWorkflow != null) {
runStrictChecks(content, parsedWorkflow, currentWorkflowId, issues, issueKeys);
@@ -394,6 +408,10 @@ public class WorkflowCheckService {
for (NodeView node : parsed.nodes) {
if (!TYPE_WORKFLOW.equals(node.type)) {
if (!TYPE_PLUGIN.equals(node.type)) {
continue;
}
checkPluginWorkflowReference(node, currentWorkflowIdString, currentContent, contentCache, issues, issueKeys);
continue;
}
String workflowId = getWorkflowIdInNode(node);
@@ -510,12 +528,86 @@ public class WorkflowCheckService {
refs.add(workflowId);
}
}
refs.addAll(workflowPluginDependencyService.extractWorkflowIdsFromPluginNodes(content));
} catch (Exception ignored) {
// ignore
}
return refs;
}
private void checkPluginWorkflowReference(NodeView node,
String currentWorkflowIdString,
String currentContent,
Map<String, String> contentCache,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
String pluginWorkflowId = getWorkflowIdInPluginNode(node);
if (!StringUtils.hasText(pluginWorkflowId)) {
addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_NOT_FOUND",
"插件节点未绑定有效工作流插件", node.id, null, node.name);
return;
}
if (StringUtils.hasText(currentWorkflowIdString) && currentWorkflowIdString.equals(pluginWorkflowId)) {
addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_CYCLE",
"插件递归引用:工作流不能通过插件引用自身", node.id, null, node.name);
return;
}
String workflowContent = loadWorkflowContent(pluginWorkflowId, currentWorkflowIdString, currentContent, contentCache);
if (!StringUtils.hasText(workflowContent)) {
addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_NOT_FOUND",
"插件绑定工作流不存在: " + pluginWorkflowId, node.id, null, node.name);
}
}
private void checkPluginSchemaHashes(ParsedWorkflow parsed,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
for (NodeView node : parsed.nodes) {
if (!TYPE_PLUGIN.equals(node.type) || node.data == null) {
continue;
}
String pluginItemId = trimToNull(node.data.getString("pluginId"));
if (!StringUtils.hasText(pluginItemId)) {
continue;
}
String workflowId = workflowPluginDependencyService.resolveWorkflowIdByPluginItemId(pluginItemId);
if (!StringUtils.hasText(workflowId)) {
continue;
}
String latestSchemaHash = resolveLatestPluginSchemaHash(pluginItemId, workflowId);
if (!StringUtils.hasText(latestSchemaHash)) {
continue;
}
String currentSchemaHash = trimToNull(node.data.getString("schemaHash"));
if (!latestSchemaHash.equals(currentSchemaHash)) {
addIssue(issues, issueKeys, "PLUGIN_SCHEMA_OUTDATED",
"当前插件节点绑定工作流的已发布参数契约已更新,请重新选择插件同步节点定义",
node.id, null, node.name);
}
}
}
private String getWorkflowIdInPluginNode(NodeView node) {
if (node == null || node.data == null) {
return null;
}
return workflowPluginDependencyService.resolveWorkflowIdByPluginItemId(
trimToNull(node.data.getString("pluginId"))
);
}
private String resolveLatestPluginSchemaHash(String pluginItemId, String workflowId) {
PluginItem pluginItem = pluginItemService.getById(pluginItemId);
if (pluginItem != null && StringUtils.hasText(pluginItem.getSchemaHash())) {
return pluginItem.getSchemaHash();
}
Workflow workflow = workflowService.getPublishedById(new BigInteger(workflowId));
if (workflow == null) {
return null;
}
return workflowPluginSnapshotResolver.resolveSchemaHash(workflow);
}
private String formatCyclePath(LinkedHashSet<String> visiting, String cycleStart) {
List<String> chain = new ArrayList<>();
boolean started = false;

View File

@@ -19,6 +19,18 @@ public class Plugin extends PluginBase {
@RelationOneToMany(selfField = "id", targetField = "pluginId", targetTable = "tb_plugin_item")
private List<PluginItem> tools;
@com.mybatisflex.annotation.Column(ignore = true)
private String workflowTitle;
@com.mybatisflex.annotation.Column(ignore = true)
private Boolean available;
@com.mybatisflex.annotation.Column(ignore = true)
private String reasonCode;
@com.mybatisflex.annotation.Column(ignore = true)
private String reasonMessage;
public String getTitle() {
return this.getName();
}
@@ -30,4 +42,36 @@ public class Plugin extends PluginBase {
public void setTools(List<PluginItem> tools) {
this.tools = tools;
}
public String getWorkflowTitle() {
return workflowTitle;
}
public void setWorkflowTitle(String workflowTitle) {
this.workflowTitle = workflowTitle;
}
public Boolean getAvailable() {
return available;
}
public void setAvailable(Boolean available) {
this.available = available;
}
public String getReasonCode() {
return reasonCode;
}
public void setReasonCode(String reasonCode) {
this.reasonCode = reasonCode;
}
public String getReasonMessage() {
return reasonMessage;
}
public void setReasonMessage(String reasonMessage) {
this.reasonMessage = reasonMessage;
}
}

View File

@@ -42,6 +42,12 @@ public class PluginBase implements Serializable {
@Column(comment = "类型")
private Integer type;
/**
* 绑定工作流ID
*/
@Column(comment = "绑定工作流ID")
private BigInteger workflowId;
/**
* 基础URL
*/
@@ -148,6 +154,14 @@ public class PluginBase implements Serializable {
this.type = type;
}
public BigInteger getWorkflowId() {
return workflowId;
}
public void setWorkflowId(BigInteger workflowId) {
this.workflowId = workflowId;
}
public String getBaseUrl() {
return baseUrl;
}

View File

@@ -90,6 +90,12 @@ public class PluginItemBase implements Serializable {
@Column(comment = "英文名称")
private String englishName;
/**
* 工作流插件输入输出契约哈希
*/
@Column(comment = "工作流插件输入输出契约哈希")
private String schemaHash;
public BigInteger getId() {
return id;
}
@@ -194,4 +200,12 @@ public class PluginItemBase implements Serializable {
this.englishName = englishName;
}
public String getSchemaHash() {
return schemaHash;
}
public void setSchemaHash(String schemaHash) {
this.schemaHash = schemaHash;
}
}

View File

@@ -0,0 +1,52 @@
package tech.easyflow.ai.enums;
/**
* 插件类型枚举。
*/
public enum PluginType {
HTTP(1),
WORKFLOW(2);
private final int code;
PluginType(int code) {
this.code = code;
}
/**
* 获取类型编码。
*
* @return 类型编码
*/
public int getCode() {
return code;
}
/**
* 根据编码解析类型,空值或未知值均按 HTTP 处理,兼容历史数据。
*
* @param code 类型编码
* @return 插件类型
*/
public static PluginType from(Integer code) {
if (code != null) {
for (PluginType value : values()) {
if (value.code == code) {
return value;
}
}
}
return HTTP;
}
/**
* 判断是否为工作流插件。
*
* @param code 类型编码
* @return 是否为工作流插件
*/
public static boolean isWorkflow(Integer code) {
return from(code) == WORKFLOW;
}
}

View File

@@ -4,11 +4,23 @@ import com.easyagents.core.model.chat.tool.Tool;
import com.alibaba.fastjson.JSON;
import com.easyagents.flow.core.chain.Chain;
import com.easyagents.flow.core.node.BaseNode;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
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.service.PluginService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.utils.WorkFlowUtil;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.util.SpringContextUtil;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.Map;
@@ -32,6 +44,11 @@ public class PluginToolNode extends BaseNode {
if (tool == null) {
return Collections.emptyMap();
}
PluginService pluginService = SpringContextUtil.getBean(PluginService.class);
Plugin plugin = pluginService.getById(tool.getPluginId());
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
return executeWorkflowPlugin(chain, map, plugin);
}
Tool function = tool.toFunction();
if (function == null) {
return Collections.emptyMap();
@@ -49,6 +66,43 @@ public class PluginToolNode extends BaseNode {
return JSON.parseObject(JSON.toJSONString(result), Map.class);
}
private Map<String, Object> executeWorkflowPlugin(Chain chain, Map<String, Object> map, Plugin plugin) {
WorkflowPluginAvailabilityService availabilityService =
SpringContextUtil.getBean(WorkflowPluginAvailabilityService.class);
LoginAccount operator = WorkFlowUtil.getOperator(chain);
WorkflowPluginAvailabilityDecision decision = availabilityService.evaluate(plugin, operator);
if (!decision.isAvailable()) {
return buildSkippedResult(decision);
}
WorkflowService workflowService = SpringContextUtil.getBean(WorkflowService.class);
Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
if (workflow == null) {
return buildSkippedResult(decision);
}
WorkflowPluginSnapshotResolver snapshotResolver = SpringContextUtil.getBean(WorkflowPluginSnapshotResolver.class);
Map<String, Object> workflowVariables = new LinkedHashMap<>();
workflowVariables.put(Constants.LOGIN_USER_KEY, operator);
if (map != null && !map.isEmpty()) {
workflowVariables.putAll(map);
}
Object result = snapshotResolver.buildWorkflowTool(workflow).invoke(workflowVariables);
if (result == null) {
return Collections.emptyMap();
}
if (result instanceof Map<?, ?> resultMap) {
return (Map<String, Object>) resultMap;
}
return JSON.parseObject(JSON.toJSONString(result), Map.class);
}
private Map<String, Object> buildSkippedResult(WorkflowPluginAvailabilityDecision decision) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("skipped", true);
result.put("reasonCode", decision.getReasonCode());
result.put("reasonMessage", decision.getReasonMessage());
return result;
}
public BigInteger getPluginId() {
return pluginId;
}

View File

@@ -0,0 +1,67 @@
package tech.easyflow.ai.plugin.workflow.availability;
/**
* 工作流插件实时可用性判定结果。
*/
public class WorkflowPluginAvailabilityDecision {
private boolean visible;
private boolean available;
private boolean snapshotPresent;
private String reasonCode;
private String reasonMessage;
private String workflowTitle;
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
public boolean isAvailable() {
return available;
}
public void setAvailable(boolean available) {
this.available = available;
}
public boolean isSnapshotPresent() {
return snapshotPresent;
}
public void setSnapshotPresent(boolean snapshotPresent) {
this.snapshotPresent = snapshotPresent;
}
public String getReasonCode() {
return reasonCode;
}
public void setReasonCode(String reasonCode) {
this.reasonCode = reasonCode;
}
public String getReasonMessage() {
return reasonMessage;
}
public void setReasonMessage(String reasonMessage) {
this.reasonMessage = reasonMessage;
}
public String getWorkflowTitle() {
return workflowTitle;
}
public void setWorkflowTitle(String workflowTitle) {
this.workflowTitle = workflowTitle;
}
}

View File

@@ -0,0 +1,28 @@
package tech.easyflow.ai.plugin.workflow.availability;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.common.entity.LoginAccount;
/**
* 工作流插件可用性判定服务。
*/
public interface WorkflowPluginAvailabilityService {
/**
* 计算当前登录用户视角下的工作流插件可见性与可用性。
*
* @param plugin 插件
* @return 判定结果
*/
WorkflowPluginAvailabilityDecision evaluateForCurrentUser(Plugin plugin);
WorkflowPluginAvailabilityDecision evaluate(Plugin plugin, LoginAccount loginAccount);
/**
* 判断当前用户是否可在管理页继续看到不可用插件。
*
* @param plugin 插件
* @return 是否允许在管理视角保留展示
*/
boolean canViewUnavailableInManagement(Plugin plugin);
}

View File

@@ -0,0 +1,126 @@
package tech.easyflow.ai.plugin.workflow.availability;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
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.Map;
/**
* 工作流插件实时可用性判定实现。
*/
@Service
public class WorkflowPluginAvailabilityServiceImpl implements WorkflowPluginAvailabilityService {
private final WorkflowService workflowService;
private final ResourceAccessService resourceAccessService;
private final CategoryPermissionService categoryPermissionService;
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
public WorkflowPluginAvailabilityServiceImpl(WorkflowService workflowService,
ResourceAccessService resourceAccessService,
CategoryPermissionService categoryPermissionService,
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver) {
this.workflowService = workflowService;
this.resourceAccessService = resourceAccessService;
this.categoryPermissionService = categoryPermissionService;
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
}
/**
* {@inheritDoc}
*/
@Override
public WorkflowPluginAvailabilityDecision evaluateForCurrentUser(Plugin plugin) {
return evaluate(plugin, SaTokenUtil.getLoginAccount());
}
@Override
public WorkflowPluginAvailabilityDecision evaluate(Plugin plugin, LoginAccount loginAccount) {
WorkflowPluginAvailabilityDecision decision = new WorkflowPluginAvailabilityDecision();
decision.setVisible(true);
decision.setAvailable(true);
if (plugin == null || !PluginType.isWorkflow(plugin.getType())) {
return decision;
}
BigInteger workflowId = plugin.getWorkflowId();
if (workflowId == null) {
return unavailable(decision, "WORKFLOW_BINDING_MISSING", "当前插件未绑定工作流");
}
Workflow workflow = workflowService.getById(workflowId);
if (workflow == null) {
return unavailable(decision, "WORKFLOW_NOT_FOUND", "当前插件绑定的工作流不存在");
}
decision.setWorkflowTitle(workflow.getTitle());
Map<String, Object> snapshot = workflow.getPublishedSnapshotJson();
boolean snapshotPresent = !CollectionUtils.isEmpty(snapshot);
decision.setSnapshotPresent(snapshotPresent);
if (!snapshotPresent) {
return unavailable(decision, "WORKFLOW_SNAPSHOT_MISSING", "当前插件绑定工作流没有可用发布快照");
}
PublishStatus publishStatus = PublishStatus.from(workflow.getPublishStatus());
if (!publishStatus.isExternallyVisible()) {
return unavailable(decision, "WORKFLOW_OFFLINE", "当前节点绑定工作流已下线");
}
Workflow publishedWorkflow = workflowService.toPublishedView(workflow);
if (!workflowPluginSnapshotResolver.isSupportedForWorkflowPlugin(publishedWorkflow)) {
return unavailable(decision, "WORKFLOW_MULTI_END_UNSUPPORTED", "当前节点绑定工作流包含多个结束节点,暂不支持作为插件使用");
}
if (!resourceAccessService.canAccess(loginAccount, CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE)) {
return unavailable(decision, "WORKFLOW_NO_PERMISSION", "当前用户无权使用目标工作流");
}
return decision;
}
/**
* {@inheritDoc}
*/
@Override
public boolean canViewUnavailableInManagement(Plugin plugin) {
if (plugin == null) {
return false;
}
if (categoryPermissionService.isCurrentSuperAdmin()) {
return true;
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (loginAccount == null || loginAccount.getId() == null || plugin.getCreatedBy() == null) {
return false;
}
return loginAccount.getId().equals(BigInteger.valueOf(plugin.getCreatedBy()));
}
/**
* 标记为不可见且不可用。
*
* @param decision 判定结果
* @param reasonCode 原因编码
* @param reasonMessage 原因说明
* @return 判定结果
*/
private WorkflowPluginAvailabilityDecision unavailable(WorkflowPluginAvailabilityDecision decision,
String reasonCode,
String reasonMessage) {
decision.setVisible(false);
decision.setAvailable(false);
decision.setReasonCode(reasonCode);
decision.setReasonMessage(reasonMessage);
return decision;
}
}

View File

@@ -0,0 +1,34 @@
package tech.easyflow.ai.plugin.workflow.binding;
import tech.easyflow.ai.entity.Plugin;
import java.math.BigInteger;
/**
* 工作流插件绑定服务。
*/
public interface WorkflowPluginBindingService {
/**
* 创建工作流插件并生成系统维护工具。
*
* @param plugin 插件
* @return 已保存插件
*/
Plugin saveWorkflowPlugin(Plugin plugin);
/**
* 更新工作流插件并刷新系统维护工具。
*
* @param plugin 插件
* @return 是否更新成功
*/
boolean updateWorkflowPlugin(Plugin plugin);
/**
* 同步某个工作流关联的所有工作流插件。
*
* @param workflowId 工作流 ID
*/
void syncByWorkflowId(BigInteger workflowId);
}

View File

@@ -0,0 +1,227 @@
package tech.easyflow.ai.plugin.workflow.binding;
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.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.mapper.PluginItemMapper;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.plugin.workflow.dependency.WorkflowPluginDependencyService;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
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.Date;
import java.util.List;
/**
* 工作流插件绑定服务实现。
*/
@Service
public class WorkflowPluginBindingServiceImpl implements WorkflowPluginBindingService {
private final PluginMapper pluginMapper;
private final PluginItemMapper pluginItemMapper;
private final WorkflowService workflowService;
private final ResourceAccessService resourceAccessService;
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
private final WorkflowPluginDependencyService workflowPluginDependencyService;
public WorkflowPluginBindingServiceImpl(PluginMapper pluginMapper,
PluginItemMapper pluginItemMapper,
WorkflowService workflowService,
ResourceAccessService resourceAccessService,
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
WorkflowPluginDependencyService workflowPluginDependencyService) {
this.pluginMapper = pluginMapper;
this.pluginItemMapper = pluginItemMapper;
this.workflowService = workflowService;
this.resourceAccessService = resourceAccessService;
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
this.workflowPluginDependencyService = workflowPluginDependencyService;
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Plugin saveWorkflowPlugin(Plugin plugin) {
LoginAccount loginAccount = requireLogin();
Workflow workflow = requirePublishedWorkflow(plugin.getWorkflowId(), "无权限绑定工作流");
normalizeWorkflowPlugin(plugin, loginAccount);
int insert = pluginMapper.insert(plugin);
if (insert <= 0) {
throw new BusinessException("保存失败");
}
PluginItem pluginItem = new PluginItem();
pluginItem.setCreated(new Date());
workflowPluginSnapshotResolver.syncPluginItemFromPublishedWorkflow(plugin, pluginItem, workflow.getId());
if (pluginItemMapper.insert(pluginItem) <= 0) {
throw new BusinessException("保存工作流插件工具失败");
}
return plugin;
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateWorkflowPlugin(Plugin plugin) {
if (plugin.getId() == null) {
throw new BusinessException("插件ID不能为空");
}
Plugin existed = pluginMapper.selectOneById(plugin.getId());
if (existed == null) {
throw new BusinessException("插件不存在");
}
if (PluginType.from(existed.getType()) != PluginType.WORKFLOW) {
throw new BusinessException("暂不支持在现有 HTTP 插件与工作流插件之间切换类型");
}
Workflow workflow = requirePublishedWorkflow(plugin.getWorkflowId(), "无权限绑定工作流");
if (workflowPluginDependencyService.containsPluginReferenceTransitivelyInPublishedSnapshot(workflow.getId(), existed.getId())) {
throw new BusinessException("目标工作流已通过子流程或插件链路引用当前插件,无法形成递归绑定");
}
normalizeWorkflowPlugin(plugin, null);
int updated = pluginMapper.update(plugin);
if (updated <= 0) {
throw new BusinessException("更新失败");
}
syncSinglePlugin(existed.getId(), workflow.getId());
return true;
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void syncByWorkflowId(BigInteger workflowId) {
if (workflowId == null) {
return;
}
QueryWrapper wrapper = QueryWrapper.create()
.eq(Plugin::getWorkflowId, workflowId)
.eq(Plugin::getType, PluginType.WORKFLOW.getCode());
List<Plugin> plugins = pluginMapper.selectListByQuery(wrapper);
for (Plugin plugin : plugins) {
syncSinglePlugin(plugin.getId(), workflowId);
}
}
private void syncSinglePlugin(BigInteger pluginId, BigInteger workflowId) {
Plugin plugin = pluginMapper.selectOneById(pluginId);
if (plugin == null) {
return;
}
Workflow workflow = requirePublishedWorkflowForSync(workflowId);
PluginItem pluginItem = getOrCreateSystemTool(pluginId);
workflowPluginSnapshotResolver.syncPluginItemFromPublishedWorkflow(plugin, pluginItem, workflow.getId());
if (pluginItem.getId() == null) {
pluginItem.setCreated(new Date());
if (pluginItemMapper.insert(pluginItem) <= 0) {
throw new BusinessException("同步工作流插件工具失败");
}
} else if (pluginItemMapper.update(pluginItem) <= 0) {
throw new BusinessException("同步工作流插件工具失败");
}
}
private PluginItem getOrCreateSystemTool(BigInteger pluginId) {
QueryWrapper wrapper = QueryWrapper.create().eq(PluginItem::getPluginId, pluginId);
List<PluginItem> pluginItems = pluginItemMapper.selectListByQuery(wrapper);
if (pluginItems == null || pluginItems.isEmpty()) {
PluginItem pluginItem = new PluginItem();
pluginItem.setPluginId(pluginId);
return pluginItem;
}
PluginItem pluginItem = pluginItems.get(0);
if (pluginItems.size() > 1) {
for (int i = 1; i < pluginItems.size(); i++) {
pluginItemMapper.deleteById(pluginItems.get(i).getId());
}
}
return pluginItem;
}
private Workflow requirePublishedWorkflow(BigInteger workflowId, String denyMessage) {
if (workflowId == null) {
throw new BusinessException("请选择已发布工作流");
}
Workflow workflow = workflowService.getById(workflowId);
if (workflow == null) {
throw new BusinessException("工作流不存在");
}
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE, denyMessage);
PublishStatus publishStatus = PublishStatus.from(workflow.getPublishStatus());
if (publishStatus != PublishStatus.PUBLISHED) {
throw new BusinessException("仅已发布工作流可被封装为插件");
}
if (workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
throw new BusinessException("目标工作流缺少已发布快照");
}
workflowPluginSnapshotResolver.assertSupportedForWorkflowPlugin(workflowService.toPublishedView(workflow));
return workflow;
}
private Workflow requirePublishedWorkflowForSync(BigInteger workflowId) {
if (workflowId == null) {
throw new BusinessException("目标工作流不存在");
}
Workflow workflow = workflowService.getById(workflowId);
if (workflow == null) {
throw new BusinessException("目标工作流不存在");
}
PublishStatus publishStatus = PublishStatus.from(workflow.getPublishStatus());
if (publishStatus != PublishStatus.PUBLISHED) {
throw new BusinessException("仅已发布工作流可同步到工作流插件");
}
if (workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
throw new BusinessException("目标工作流缺少已发布快照");
}
workflowPluginSnapshotResolver.assertSupportedForWorkflowPlugin(workflowService.toPublishedView(workflow));
return workflow;
}
private void normalizeWorkflowPlugin(Plugin plugin, LoginAccount loginAccount) {
plugin.setType(PluginType.WORKFLOW.getCode());
plugin.setBaseUrl(null);
plugin.setAuthType(null);
plugin.setPosition(null);
plugin.setHeaders(null);
plugin.setTokenKey(null);
plugin.setTokenValue(null);
if (plugin.getCreated() == null) {
plugin.setCreated(new Date());
}
if (loginAccount != null) {
plugin.setCreatedBy(loginAccount.getId().longValue());
plugin.setDeptId(loginAccount.getDeptId() == null ? null : loginAccount.getDeptId().longValue());
plugin.setTenantId(loginAccount.getTenantId() == null ? null : loginAccount.getTenantId().longValue());
}
if (!StringUtils.hasText(plugin.getAlias())) {
plugin.setAlias(null);
}
}
private LoginAccount requireLogin() {
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (loginAccount == null || loginAccount.getId() == null) {
throw new BusinessException("当前未登录");
}
return loginAccount;
}
}

View File

@@ -0,0 +1,73 @@
package tech.easyflow.ai.plugin.workflow.dependency;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import java.math.BigInteger;
import java.util.List;
import java.util.Set;
/**
* 工作流插件依赖分析服务。
*/
public interface WorkflowPluginDependencyService {
/**
* 查询被某个工作流引用的插件列表。
*
* @param workflowId 工作流 ID
* @return 插件引用列表
*/
List<OfflineImpactBindingVo> listPluginsByWorkflowId(BigInteger workflowId);
/**
* 解析工作流内容中通过插件间接引用到的工作流 ID。
*
* @param content 工作流内容
* @return 引用到的工作流 ID 集合
*/
Set<String> extractWorkflowIdsFromPluginNodes(String content);
/**
* 判断工作流内容是否引用了指定插件。
*
* @param content 工作流内容
* @param pluginId 插件 ID
* @return 是否引用
*/
boolean containsPluginReference(String content, BigInteger pluginId);
/**
* 判断某个工作流是否经由子流程/工作流插件链路递归引用了指定插件。
*
* @param workflowId 工作流 ID
* @param pluginId 插件 ID
* @return 是否存在递归引用
*/
boolean containsPluginReferenceTransitively(BigInteger workflowId, BigInteger pluginId);
/**
* 判断某个工作流的已发布快照是否经由子流程/工作流插件链路递归引用了指定插件。
*
* @param workflowId 工作流 ID
* @param pluginId 插件 ID
* @return 是否存在递归引用
*/
boolean containsPluginReferenceTransitivelyInPublishedSnapshot(BigInteger workflowId, BigInteger pluginId);
/**
* 查询工作流插件。
*
* @param pluginId 插件 ID
* @return 插件
*/
Plugin getWorkflowPlugin(BigInteger pluginId);
/**
* 根据插件工具 ID 解析目标工作流 ID。
*
* @param pluginItemId 插件工具 ID
* @return 工作流 ID
*/
String resolveWorkflowIdByPluginItemId(String pluginItemId);
}

View File

@@ -0,0 +1,334 @@
package tech.easyflow.ai.plugin.workflow.dependency;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.mapper.PluginItemMapper;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 工作流插件依赖分析实现。
*/
@Service
public class WorkflowPluginDependencyServiceImpl implements WorkflowPluginDependencyService {
private static final String TYPE_PLUGIN = "plugin-node";
private static final String TYPE_WORKFLOW = "workflow-node";
private final PluginMapper pluginMapper;
private final PluginItemMapper pluginItemMapper;
private final WorkflowService workflowService;
public WorkflowPluginDependencyServiceImpl(PluginMapper pluginMapper,
PluginItemMapper pluginItemMapper,
WorkflowService workflowService) {
this.pluginMapper = pluginMapper;
this.pluginItemMapper = pluginItemMapper;
this.workflowService = workflowService;
}
/**
* {@inheritDoc}
*/
@Override
public List<OfflineImpactBindingVo> listPluginsByWorkflowId(BigInteger workflowId) {
if (workflowId == null) {
return Collections.emptyList();
}
QueryWrapper wrapper = QueryWrapper.create().eq(Plugin::getWorkflowId, workflowId);
List<Plugin> plugins = pluginMapper.selectListByQuery(wrapper);
if (plugins == null || plugins.isEmpty()) {
return Collections.emptyList();
}
List<OfflineImpactBindingVo> result = new ArrayList<>(plugins.size());
for (Plugin plugin : plugins) {
if (!PluginType.isWorkflow(plugin.getType())) {
continue;
}
OfflineImpactBindingVo vo = new OfflineImpactBindingVo();
vo.setId(plugin.getId());
vo.setTitle(plugin.getName());
result.add(vo);
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> extractWorkflowIdsFromPluginNodes(String content) {
if (!StringUtils.hasText(content)) {
return Collections.emptySet();
}
Set<String> workflowIds = new LinkedHashSet<>();
try {
Object parsed = JSON.parse(content);
if (!(parsed instanceof JSONObject root)) {
return workflowIds;
}
JSONArray nodes = root.getJSONArray("nodes");
if (nodes == null) {
return workflowIds;
}
for (int i = 0; i < nodes.size(); i++) {
JSONObject node = nodes.getJSONObject(i);
if (node == null || !TYPE_PLUGIN.equals(node.getString("type"))) {
continue;
}
JSONObject data = node.getJSONObject("data");
String pluginItemId = data == null ? null : data.getString("pluginId");
if (!StringUtils.hasText(pluginItemId)) {
continue;
}
Plugin plugin = getPluginByPluginItemId(pluginItemId);
if (plugin == null || !PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
continue;
}
workflowIds.add(String.valueOf(plugin.getWorkflowId()));
}
} catch (Exception ignored) {
// ignore
}
return workflowIds;
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsPluginReference(String content, BigInteger pluginId) {
if (!StringUtils.hasText(content) || pluginId == null) {
return false;
}
try {
Object parsed = JSON.parse(content);
if (!(parsed instanceof JSONObject root)) {
return false;
}
JSONArray nodes = root.getJSONArray("nodes");
if (nodes == null) {
return false;
}
String expected = String.valueOf(pluginId);
for (int i = 0; i < nodes.size(); i++) {
JSONObject node = nodes.getJSONObject(i);
if (node == null || !TYPE_PLUGIN.equals(node.getString("type"))) {
continue;
}
JSONObject data = node.getJSONObject("data");
String pluginItemId = data == null ? null : data.getString("pluginId");
Plugin plugin = getPluginByPluginItemId(pluginItemId);
if (plugin != null && expected.equals(String.valueOf(plugin.getId()))) {
return true;
}
}
} catch (Exception ignored) {
// ignore
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsPluginReferenceTransitively(BigInteger workflowId, BigInteger pluginId) {
if (workflowId == null || pluginId == null) {
return false;
}
return containsPluginReferenceTransitively(
String.valueOf(workflowId),
pluginId,
false,
new LinkedHashSet<>(),
new HashMap<>()
);
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsPluginReferenceTransitivelyInPublishedSnapshot(BigInteger workflowId, BigInteger pluginId) {
if (workflowId == null || pluginId == null) {
return false;
}
return containsPluginReferenceTransitively(
String.valueOf(workflowId),
pluginId,
true,
new LinkedHashSet<>(),
new HashMap<>()
);
}
/**
* {@inheritDoc}
*/
@Override
public Plugin getWorkflowPlugin(BigInteger pluginId) {
if (pluginId == null) {
return null;
}
Plugin plugin = pluginMapper.selectOneById(pluginId);
if (plugin == null || !PluginType.isWorkflow(plugin.getType())) {
return null;
}
return plugin;
}
/**
* {@inheritDoc}
*/
@Override
public String resolveWorkflowIdByPluginItemId(String pluginItemId) {
Plugin plugin = getPluginByPluginItemId(pluginItemId);
if (plugin == null || !PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
return null;
}
return String.valueOf(plugin.getWorkflowId());
}
private Plugin getPluginByPluginItemId(String pluginItemId) {
PluginItem pluginItem = pluginItemMapper.selectOneById(pluginItemId);
if (pluginItem == null || pluginItem.getPluginId() == null) {
return null;
}
return pluginMapper.selectOneById(pluginItem.getPluginId());
}
private boolean containsPluginReferenceTransitively(String workflowId,
BigInteger pluginId,
boolean publishedOnly,
Set<String> visitingWorkflowIds,
Map<String, Boolean> cache) {
if (!StringUtils.hasText(workflowId)) {
return false;
}
Boolean cached = cache.get(workflowId);
if (cached != null) {
return cached;
}
if (!visitingWorkflowIds.add(workflowId)) {
return false;
}
boolean result = false;
try {
String workflowContent = resolveWorkflowContent(workflowId, publishedOnly);
if (!StringUtils.hasText(workflowContent)) {
cache.put(workflowId, false);
return false;
}
result = containsPluginReferenceTransitivelyInContent(
workflowContent,
pluginId,
publishedOnly,
visitingWorkflowIds,
cache
);
cache.put(workflowId, result);
return result;
} finally {
visitingWorkflowIds.remove(workflowId);
}
}
private boolean containsPluginReferenceTransitivelyInContent(String content,
BigInteger pluginId,
boolean publishedOnly,
Set<String> visitingWorkflowIds,
Map<String, Boolean> cache) {
if (!StringUtils.hasText(content) || pluginId == null) {
return false;
}
try {
Object parsed = JSON.parse(content);
if (!(parsed instanceof JSONObject root)) {
return false;
}
JSONArray nodes = root.getJSONArray("nodes");
if (nodes == null || nodes.isEmpty()) {
return false;
}
String expectedPluginId = String.valueOf(pluginId);
for (int i = 0; i < nodes.size(); i++) {
JSONObject node = nodes.getJSONObject(i);
if (node == null) {
continue;
}
String nodeType = node.getString("type");
JSONObject data = node.getJSONObject("data");
if (TYPE_PLUGIN.equals(nodeType)) {
String pluginItemId = data == null ? null : data.getString("pluginId");
Plugin plugin = getPluginByPluginItemId(pluginItemId);
if (plugin == null) {
continue;
}
if (expectedPluginId.equals(String.valueOf(plugin.getId()))) {
return true;
}
if (PluginType.isWorkflow(plugin.getType())
&& plugin.getWorkflowId() != null
&& containsPluginReferenceTransitively(
String.valueOf(plugin.getWorkflowId()),
pluginId,
publishedOnly,
visitingWorkflowIds,
cache
)) {
return true;
}
continue;
}
if (TYPE_WORKFLOW.equals(nodeType)) {
String refWorkflowId = data == null ? null : data.getString("workflowId");
if (containsPluginReferenceTransitively(
refWorkflowId,
pluginId,
publishedOnly,
visitingWorkflowIds,
cache
)) {
return true;
}
}
}
} catch (Exception ignored) {
// ignore
}
return false;
}
private String resolveWorkflowContent(String workflowId, boolean publishedOnly) {
tech.easyflow.ai.entity.Workflow workflow = publishedOnly
? getPublishedWorkflow(workflowId)
: workflowService.getById(workflowId);
return workflow == null ? null : workflow.getContent();
}
private tech.easyflow.ai.entity.Workflow getPublishedWorkflow(String workflowId) {
try {
return workflowService.getPublishedById(new BigInteger(workflowId));
} catch (Exception ignored) {
return null;
}
}
}

View File

@@ -0,0 +1,231 @@
package tech.easyflow.ai.plugin.workflow.snapshot;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Node;
import com.easyagents.flow.core.node.EndNode;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.service.WorkflowService;
import com.easyagents.flow.core.parser.ChainParser;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.util.List;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 工作流插件快照解析服务。
*/
@Service
public class WorkflowPluginSnapshotResolver {
private final WorkflowService workflowService;
private final ChainParser chainParser;
private final WorkflowDatacenterContentService workflowDatacenterContentService;
public WorkflowPluginSnapshotResolver(WorkflowService workflowService,
ChainParser chainParser,
WorkflowDatacenterContentService workflowDatacenterContentService) {
this.workflowService = workflowService;
this.chainParser = chainParser;
this.workflowDatacenterContentService = workflowDatacenterContentService;
}
/**
* 使用已发布视图刷新工作流插件工具定义。
*
* @param plugin 插件
* @param pluginItem 插件工具
* @param workflowId 工作流 ID
*/
public void syncPluginItemFromPublishedWorkflow(Plugin plugin, PluginItem pluginItem, java.math.BigInteger workflowId) {
Workflow workflow = workflowService.getPublishedById(workflowId);
if (workflow == null) {
throw new BusinessException("工作流不存在");
}
assertSupportedForWorkflowPlugin(workflow);
ChainDefinition definition = parseDefinition(workflow);
pluginItem.setPluginId(plugin.getId());
pluginItem.setName(plugin.getName());
pluginItem.setEnglishName(workflow.getEnglishName());
pluginItem.setDescription(resolveDescription(plugin, workflow));
pluginItem.setBasePath(null);
pluginItem.setRequestMethod("WORKFLOW");
JSONArray inputDefinitions = resolveInputDefinitions(definition);
JSONArray outputDefinitions = resolveOutputDefinitions(definition);
pluginItem.setInputData(JSON.toJSONString(inputDefinitions));
pluginItem.setOutputData(JSON.toJSONString(outputDefinitions));
pluginItem.setSchemaHash(resolveSchemaHash(inputDefinitions, outputDefinitions));
pluginItem.setServiceStatus(1);
pluginItem.setStatus(1);
}
/**
* 构建工作流插件运行工具。
*
* @param workflow 工作流已发布视图
* @return 工作流工具
*/
public WorkflowTool buildWorkflowTool(Workflow workflow) {
return new WorkflowTool(
workflow,
false,
PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId()))
);
}
/**
* 解析输入参数定义。
*
* @param definition 流程定义
* @return 输入参数数组
*/
public JSONArray resolveInputDefinitions(ChainDefinition definition) {
JSONArray inputs = JSON.parseArray(JSON.toJSONString(definition.getStartParameters()));
markWorkflowPluginInput(inputs);
return inputs == null ? new JSONArray() : inputs;
}
/**
* 解析输出参数定义。
*
* @param definition 流程定义
* @return 输出参数数组
*/
public JSONArray resolveOutputDefinitions(ChainDefinition definition) {
JSONArray outputs = new JSONArray();
List<Node> nodes = definition.getNodes();
if (nodes == null) {
return outputs;
}
for (Node node : nodes) {
if (node instanceof EndNode endNode) {
outputs = JSON.parseArray(JSON.toJSONString(endNode.getOutputDefs()));
}
}
return outputs == null ? new JSONArray() : outputs;
}
/**
* 解析工作流插件契约哈希。
*
* @param workflow 工作流
* @return 哈希值
*/
public String resolveSchemaHash(Workflow workflow) {
ChainDefinition definition = parseDefinition(workflow);
return resolveSchemaHash(resolveInputDefinitions(definition), resolveOutputDefinitions(definition));
}
/**
* 解析工作流插件契约哈希。
*
* @param inputDefinitions 输入定义
* @param outputDefinitions 输出定义
* @return 哈希值
*/
public String resolveSchemaHash(JSONArray inputDefinitions, JSONArray outputDefinitions) {
JSONObject payload = new JSONObject();
payload.put("inputs", inputDefinitions == null ? new JSONArray() : inputDefinitions);
payload.put("outputs", outputDefinitions == null ? new JSONArray() : outputDefinitions);
return sha256Hex(JSON.toJSONString(payload));
}
/**
* 校验工作流是否支持被封装为工作流插件。
*
* @param workflow 工作流
*/
public void assertSupportedForWorkflowPlugin(Workflow workflow) {
ChainDefinition definition = parseDefinition(workflow);
int endNodeCount = countEndNodes(definition);
// 一期先收敛为“单结束节点才能封装为插件”,后续若要支持多结束节点,
// 需要先补齐统一输出契约、父流程节点 schema 同步和结果展示策略。
if (endNodeCount != 1) {
throw new BusinessException("工作流插件仅支持单一结束节点,当前工作流不可封装为插件");
}
}
/**
* 判断工作流是否为单结束节点结构。
*
* @param workflow 工作流
* @return 单结束节点返回 true
*/
public boolean isSupportedForWorkflowPlugin(Workflow workflow) {
ChainDefinition definition = parseDefinition(workflow);
return countEndNodes(definition) == 1;
}
/**
* 解析已发布工作流定义。
*
* @param workflow 工作流已发布视图
* @return 流程定义
*/
public ChainDefinition parseDefinition(Workflow workflow) {
String preparedContent = workflowDatacenterContentService.prepareContent(workflow.getContent());
return chainParser.parse(preparedContent);
}
private String resolveDescription(Plugin plugin, Workflow workflow) {
if (plugin.getDescription() != null && !plugin.getDescription().isBlank()) {
return plugin.getDescription();
}
return workflow.getDescription();
}
private void markWorkflowPluginInput(JSONArray parameters) {
if (parameters == null) {
return;
}
for (Object parameter : parameters) {
if (!(parameter instanceof com.alibaba.fastjson2.JSONObject obj)) {
continue;
}
obj.put("refType", "ref");
JSONArray children = obj.getJSONArray("children");
if (children != null) {
markWorkflowPluginInput(children);
}
}
}
private int countEndNodes(ChainDefinition definition) {
List<Node> nodes = definition == null ? null : definition.getNodes();
if (nodes == null || nodes.isEmpty()) {
return 0;
}
int count = 0;
for (Node node : nodes) {
if (node instanceof EndNode) {
count++;
}
}
return count;
}
private String sha256Hex(String source) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] bytes = digest.digest(source.getBytes(StandardCharsets.UTF_8));
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte current : bytes) {
builder.append(Character.forDigit((current >> 4) & 0xF, 16));
builder.append(Character.forDigit(current & 0xF, 16));
}
return builder.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 algorithm unavailable", e);
}
}
}

View File

@@ -6,6 +6,8 @@ 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;
@@ -31,18 +33,24 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
private final ResourceAccessService resourceAccessService;
private final BotWorkflowService botWorkflowService;
private final ResourceOfflineImpactService resourceOfflineImpactService;
private final WorkflowPluginBindingService workflowPluginBindingService;
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
ResourceAccessService resourceAccessService,
ApprovalInstanceService approvalInstanceService,
BotWorkflowService botWorkflowService,
ResourceOfflineImpactService resourceOfflineImpactService,
WorkflowPluginBindingService workflowPluginBindingService,
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
ObjectMapper objectMapper) {
super(approvalInstanceService, objectMapper);
this.workflowService = workflowService;
this.resourceAccessService = resourceAccessService;
this.botWorkflowService = botWorkflowService;
this.resourceOfflineImpactService = resourceOfflineImpactService;
this.workflowPluginBindingService = workflowPluginBindingService;
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
}
@Override
@@ -116,6 +124,16 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
return snapshot;
}
@Override
protected Map<String, Object> buildPublishSnapshot(Workflow resource, PublishStatus currentStatus) {
Map<String, Object> snapshot = super.buildPublishSnapshot(resource, currentStatus);
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkWorkflowImpact(resource.getId());
if (impact.isHasPluginBindings()) {
workflowPluginSnapshotResolver.assertSupportedForWorkflowPlugin(resource);
}
return snapshot;
}
@Override
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
Workflow update = new Workflow();
@@ -135,6 +153,7 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
update.setPublishedAt(new java.util.Date());
update.setPublishedBy(operatorId);
workflowService.updateById(update);
workflowPluginBindingService.syncByWorkflowId(resourceId);
}
@Override
@@ -162,6 +181,9 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
if (impact.isHasBotBindings()) {
snapshot.put("botBindings", impact.getBotBindings());
}
if (impact.isHasPluginBindings()) {
snapshot.put("pluginBindings", impact.getPluginBindings());
}
}
@Override

View File

@@ -28,4 +28,12 @@ public interface PluginItemService extends IService<PluginItem> {
Result pluginToolTest(String inputData, BigInteger pluginToolId);
List<PluginItem> getByPluginId(String id);
/**
* 获取某个插件的系统维护工具。
*
* @param pluginId 插件 ID
* @return 工具
*/
PluginItem getSingleByPluginId(BigInteger pluginId);
}

View File

@@ -23,4 +23,22 @@ public interface PluginService extends IService<Plugin> {
Result pageByCategory(Long pageNumber, Long pageSize, int category);
boolean updatePlugin(Plugin plugin);
/**
* 按当前用户视角过滤并补充工作流插件可用性信息。
*
* @param plugins 插件列表
* @param managementView 是否为管理视角
* @param availableOnly 是否仅保留当前可用插件
* @return 过滤后的插件列表
*/
List<Plugin> preparePluginsForCurrentUser(List<Plugin> plugins, boolean managementView, boolean availableOnly);
/**
* 补充单个插件的工作流可用性信息。
*
* @param plugin 插件
* @return 原插件
*/
Plugin preparePluginForCurrentUser(Plugin plugin);
}

View File

@@ -31,6 +31,7 @@ import tech.easyflow.ai.easyagents.memory.RuntimeChatMemory;
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.mapper.BotMapper;
import tech.easyflow.ai.service.*;
@@ -117,6 +118,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
@Resource
private BotPluginService botPluginService;
@Resource
private PluginService pluginService;
@Resource
private PluginItemService pluginItemService;
@Resource
private BotMcpService botMcpService;
@@ -508,6 +511,12 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
List<PluginItem> pluginItems = pluginItemService.getMapper().selectListWithRelationsByQuery(queryTool);
if (pluginItems != null && !pluginItems.isEmpty()) {
for (PluginItem pluginItem : pluginItems) {
if (pluginItem.getPluginId() != null) {
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
continue;
}
}
functionList.add(pluginItem.toFunction());
}
}

View File

@@ -1,15 +1,21 @@
package tech.easyflow.ai.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginCategory;
import tech.easyflow.ai.entity.PluginCategoryMapping;
import tech.easyflow.ai.mapper.PluginCategoryMapper;
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.service.PluginCategoryMappingService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import javax.annotation.Resource;
import java.math.BigInteger;
@@ -34,6 +40,12 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
@Resource
private PluginCategoryMapper pluginCategoryMapper;
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginVisibilityService pluginVisibilityService;
@Resource
private CategoryPermissionService categoryPermissionService;
@Override
@Transactional
@@ -41,6 +53,11 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
if (pluginId == null) {
throw new BusinessException("插件ID不能为空");
}
Plugin plugin = pluginMapper.selectOneById(pluginId);
if (plugin == null) {
throw new BusinessException("插件不存在");
}
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), pluginId, "无权限访问插件");
List<BigInteger> targetCategoryIds = categoryIds == null
? Collections.emptyList()
@@ -48,6 +65,8 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
.filter(java.util.Objects::nonNull)
.distinct()
.collect(Collectors.toList());
assertCategoryIdsValid(targetCategoryIds);
assertCategoryAccess(targetCategoryIds);
QueryWrapper currentRelationQuery = QueryWrapper.create().select("category_id")
.from("tb_plugin_category_mapping")
@@ -80,6 +99,37 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
return true;
}
private void assertCategoryIdsValid(List<BigInteger> targetCategoryIds) {
if (CollectionUtil.isEmpty(targetCategoryIds)) {
return;
}
QueryWrapper queryWrapper = QueryWrapper.create()
.select(PluginCategory::getId)
.in(PluginCategory::getId, targetCategoryIds);
List<BigInteger> existedCategoryIds = pluginCategoryMapper.selectListByQueryAs(queryWrapper, BigInteger.class);
if (existedCategoryIds == null || existedCategoryIds.size() != new LinkedHashSet<>(targetCategoryIds).size()) {
throw new BusinessException("存在无效的插件分类");
}
}
private void assertCategoryAccess(List<BigInteger> targetCategoryIds) {
if (CollectionUtil.isEmpty(targetCategoryIds)) {
return;
}
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
if (!access.isRestricted()) {
return;
}
Set<BigInteger> allowedCategoryIds = access.getCategoryIds();
if (CollectionUtil.isEmpty(allowedCategoryIds)) {
throw new BusinessException("无权限关联所选插件分类");
}
boolean allAllowed = targetCategoryIds.stream().allMatch(allowedCategoryIds::contains);
if (!allAllowed) {
throw new BusinessException("无权限关联所选插件分类");
}
}
@Override
public List<PluginCategory> getPluginCategories(BigInteger pluginId) {
QueryWrapper categoryQueryWrapper = QueryWrapper.create().select("category_id")

View File

@@ -5,14 +5,22 @@ import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.entity.BotPlugin;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.easyagents.tool.PluginTool;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
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.common.domain.Result;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.domain.Result;
import javax.annotation.Resource;
import java.math.BigInteger;
@@ -37,9 +45,19 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
@Resource
private BotPluginMapper botPluginMapper;
@Resource
private WorkflowPluginAvailabilityService workflowPluginAvailabilityService;
@Resource
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
@Resource
private WorkflowService workflowService;
@Override
public boolean savePluginTool(PluginItem pluginItem) {
Plugin plugin = pluginMapper.selectOneById(pluginItem.getPluginId());
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
throw new BusinessException("工作流插件工具由系统自动维护,不支持手动新增");
}
pluginItem.setCreated(new Date());
pluginItem.setRequestMethod("Post");
int insert = pluginItemMapper.insert(pluginItem);
@@ -61,14 +79,20 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
.select()
.eq(Plugin::getId, pluginItem.getPluginId());
Plugin plugin = pluginMapper.selectOneByQuery(queryAiPluginWrapper);
plugin = preparePluginForCurrentUser(plugin);
Map<String, Object> result = new HashMap<>();
result.put("data", pluginItem);
result.put("aiPlugin", plugin);
result.put("workflowSnapshot", buildWorkflowSnapshot(plugin));
return Result.ok(result);
}
@Override
public boolean updatePlugin(PluginItem pluginItem) {
Plugin existedPlugin = resolvePluginByPluginItemId(pluginItem.getId());
if (existedPlugin != null && PluginType.isWorkflow(existedPlugin.getType())) {
throw new BusinessException("工作流插件工具由系统自动维护,不支持手动修改");
}
int update = pluginItemMapper.update(pluginItem);
if (update <= 0) {
throw new BusinessException("修改失败");
@@ -113,10 +137,37 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
@Override
public Result<?> pluginToolTest(String inputData, BigInteger pluginToolId) {
PluginItem pluginItem = new PluginItem();
pluginItem.setId(pluginToolId);
pluginItem.setInputData(inputData);
PluginTool pluginTool = new PluginTool(pluginItem);
PluginItem pluginItem = pluginItemMapper.selectOneById(pluginToolId);
if (pluginItem == null) {
throw new BusinessException("插件工具不存在");
}
Plugin plugin = pluginMapper.selectOneById(pluginItem.getPluginId());
if (plugin == null) {
throw new BusinessException("插件不存在");
}
plugin = preparePluginForCurrentUser(plugin);
if (PluginType.isWorkflow(plugin.getType())) {
WorkflowPluginAvailabilityDecision decision = workflowPluginAvailabilityService.evaluateForCurrentUser(plugin);
if (!decision.isAvailable()) {
return Result.ok(buildUnavailableResult(decision));
}
Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
if (workflow == null) {
return Result.ok(buildUnavailableResult(decision));
}
Map<String, Object> args = com.alibaba.fastjson2.JSON.parseObject(inputData, Map.class);
Map<String, Object> variables = new LinkedHashMap<>();
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (loginAccount != null) {
variables.put(Constants.LOGIN_USER_KEY, loginAccount);
}
if (args != null && !args.isEmpty()) {
variables.putAll(args);
}
Object result = workflowPluginSnapshotResolver.buildWorkflowTool(workflow).invoke(variables);
return Result.ok(result);
}
tech.easyflow.ai.easyagents.tool.PluginTool pluginTool = new tech.easyflow.ai.easyagents.tool.PluginTool(pluginItem);
return Result.ok(pluginTool.runPluginTool(null, inputData, pluginToolId));
}
@@ -129,4 +180,82 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
return list(queryWrapper);
}
/**
* {@inheritDoc}
*/
@Override
public PluginItem getSingleByPluginId(BigInteger pluginId) {
if (pluginId == null) {
return null;
}
QueryWrapper queryWrapper = QueryWrapper.create().eq(PluginItem::getPluginId, pluginId);
List<PluginItem> items = pluginItemMapper.selectListByQuery(queryWrapper);
return items == null || items.isEmpty() ? null : items.get(0);
}
/**
* 按当前用户视角补充单个插件的可用性信息。
*
* @param plugin 插件
* @return 已补充可用性信息的插件
*/
private Plugin preparePluginForCurrentUser(Plugin plugin) {
if (plugin == null) {
return null;
}
plugin.setType(PluginType.from(plugin.getType()).getCode());
if (!PluginType.isWorkflow(plugin.getType())) {
plugin.setAvailable(true);
plugin.setReasonCode(null);
plugin.setReasonMessage(null);
return plugin;
}
WorkflowPluginAvailabilityDecision decision = workflowPluginAvailabilityService.evaluateForCurrentUser(plugin);
plugin.setWorkflowTitle(decision.getWorkflowTitle());
plugin.setAvailable(decision.isAvailable());
plugin.setReasonCode(decision.getReasonCode());
plugin.setReasonMessage(decision.getReasonMessage());
return plugin;
}
private Plugin resolvePluginByPluginItemId(BigInteger pluginItemId) {
if (pluginItemId == null) {
return null;
}
PluginItem existed = pluginItemMapper.selectOneById(pluginItemId);
if (existed == null || existed.getPluginId() == null) {
return null;
}
return pluginMapper.selectOneById(existed.getPluginId());
}
private Map<String, Object> buildUnavailableResult(WorkflowPluginAvailabilityDecision decision) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("skipped", true);
result.put("reasonCode", decision.getReasonCode());
result.put("reasonMessage", decision.getReasonMessage());
return result;
}
/**
* 构建工作流插件对应的已发布快照摘要。
*
* @param plugin 插件
* @return 快照摘要,不存在时返回 {@code null}
*/
private Map<String, Object> buildWorkflowSnapshot(Plugin plugin) {
if (plugin == null || !PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
return null;
}
Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
if (workflow == null) {
return null;
}
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("title", workflow.getTitle());
snapshot.put("description", workflow.getDescription());
snapshot.put("content", workflow.getContent());
return snapshot;
}
}

View File

@@ -9,14 +9,20 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
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.common.domain.Result;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
@@ -60,9 +66,18 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
private CategoryPermissionService categoryPermissionService;
@Resource
private PluginVisibilityService pluginVisibilityService;
@Resource
private WorkflowPluginBindingService workflowPluginBindingService;
@Resource
private WorkflowPluginAvailabilityService workflowPluginAvailabilityService;
@Override
public Plugin savePlugin(Plugin plugin) {
PluginType pluginType = PluginType.from(plugin.getType());
if (pluginType == PluginType.WORKFLOW) {
return workflowPluginBindingService.saveWorkflowPlugin(plugin);
}
normalizeHttpPlugin(plugin, true);
plugin.setCreated(new Date());
int insert = pluginMapper.insert(plugin);
if (insert <= 0) {
@@ -143,23 +158,89 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, 0L));
}
List<Plugin> totalPlugins = preparePluginsForCurrentUser(queryPluginsByIds(visiblePluginIds), true, false);
int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize));
if (fromIndex >= visiblePluginIds.size()) {
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, visiblePluginIds.size()));
if (fromIndex >= totalPlugins.size()) {
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, totalPlugins.size()));
}
int toIndex = Math.min(visiblePluginIds.size(), Math.toIntExact(fromIndex + pageSize));
List<BigInteger> currentPagePluginIds = new ArrayList<>(visiblePluginIds.subList(fromIndex, toIndex));
List<Plugin> plugins = queryPluginsByIds(currentPagePluginIds);
Page<Plugin> aiPluginPage = new Page<>(plugins, pageNumber, pageSize, visiblePluginIds.size());
int toIndex = Math.min(totalPlugins.size(), Math.toIntExact(fromIndex + pageSize));
Page<Plugin> aiPluginPage = new Page<>(new ArrayList<>(totalPlugins.subList(fromIndex, toIndex)),
pageNumber, pageSize, totalPlugins.size());
return Result.ok(aiPluginPage);
}
@Override
public boolean updatePlugin(Plugin plugin) {
Plugin existed = pluginMapper.selectOneById(plugin.getId());
if (existed == null) {
throw new BusinessException("插件不存在");
}
PluginType existedType = PluginType.from(existed.getType());
PluginType targetType = PluginType.from(plugin.getType() == null ? existed.getType() : plugin.getType());
if (existedType != targetType) {
throw new BusinessException("暂不支持切换插件类型");
}
if (targetType == PluginType.WORKFLOW) {
return workflowPluginBindingService.updateWorkflowPlugin(plugin);
}
normalizeHttpPlugin(plugin, false);
pluginMapper.update(plugin);
return true;
}
/**
* {@inheritDoc}
*/
@Override
public List<Plugin> preparePluginsForCurrentUser(List<Plugin> plugins, boolean managementView, boolean availableOnly) {
if (plugins == null || plugins.isEmpty()) {
return Collections.emptyList();
}
List<Plugin> result = new ArrayList<>();
for (Plugin plugin : plugins) {
Plugin prepared = preparePluginForCurrentUser(plugin);
if (!PluginType.isWorkflow(prepared.getType())) {
result.add(prepared);
continue;
}
boolean canKeepUnavailable = managementView && workflowPluginAvailabilityService.canViewUnavailableInManagement(prepared);
if (Boolean.TRUE.equals(prepared.getAvailable())) {
result.add(prepared);
continue;
}
if (availableOnly) {
continue;
}
if (canKeepUnavailable) {
result.add(prepared);
}
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public Plugin preparePluginForCurrentUser(Plugin plugin) {
if (plugin == null) {
return null;
}
plugin.setType(PluginType.from(plugin.getType()).getCode());
if (!PluginType.isWorkflow(plugin.getType())) {
plugin.setAvailable(true);
plugin.setReasonCode(null);
plugin.setReasonMessage(null);
return plugin;
}
WorkflowPluginAvailabilityDecision decision = workflowPluginAvailabilityService.evaluateForCurrentUser(plugin);
plugin.setWorkflowTitle(decision.getWorkflowTitle());
plugin.setAvailable(decision.isAvailable());
plugin.setReasonCode(decision.getReasonCode());
plugin.setReasonMessage(decision.getReasonMessage());
return plugin;
}
private List<BigInteger> queryCreatorPluginIds(List<BigInteger> pluginIds, Long creatorId) {
if (CollectionUtil.isEmpty(pluginIds) || creatorId == null) {
return Collections.emptyList();
@@ -175,7 +256,7 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
return Collections.emptyList();
}
QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds);
List<Plugin> plugins = pluginMapper.selectListByQuery(queryPluginWrapper);
List<Plugin> plugins = pluginMapper.selectListWithRelationsByQuery(queryPluginWrapper);
Map<BigInteger, Plugin> pluginMap = plugins.stream().collect(Collectors.toMap(
Plugin::getId,
item -> item,
@@ -192,5 +273,24 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
return orderedPlugins;
}
/**
* 归一化 HTTP 插件基础字段。
*
* @param plugin 插件
* @param isSave 是否为创建
*/
private void normalizeHttpPlugin(Plugin plugin, boolean isSave) {
plugin.setType(PluginType.HTTP.getCode());
plugin.setWorkflowId(null);
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (isSave && loginAccount != null && loginAccount.getId() != null) {
plugin.setCreatedBy(loginAccount.getId().longValue());
plugin.setDeptId(loginAccount.getDeptId() == null ? null : loginAccount.getDeptId().longValue());
plugin.setTenantId(loginAccount.getTenantId() == null ? null : loginAccount.getTenantId().longValue());
}
if (plugin.getHeaders() != null && !(plugin.getHeaders() instanceof String)) {
plugin.setHeaders(plugin.getHeaders().toString());
}
}
}

View File

@@ -11,6 +11,7 @@ import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.BotWorkflow;
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;
@@ -49,17 +50,20 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
private final BotService botService;
private final WorkflowService workflowService;
private final RedisLockExecutor redisLockExecutor;
private final WorkflowPluginDependencyService workflowPluginDependencyService;
public ResourceOfflineImpactServiceImpl(BotWorkflowService botWorkflowService,
BotDocumentCollectionService botDocumentCollectionService,
BotService botService,
WorkflowService workflowService,
RedisLockExecutor redisLockExecutor) {
RedisLockExecutor redisLockExecutor,
WorkflowPluginDependencyService workflowPluginDependencyService) {
this.botWorkflowService = botWorkflowService;
this.botDocumentCollectionService = botDocumentCollectionService;
this.botService = botService;
this.workflowService = workflowService;
this.redisLockExecutor = redisLockExecutor;
this.workflowPluginDependencyService = workflowPluginDependencyService;
}
/**
@@ -68,15 +72,16 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
@Override
public OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId) {
List<OfflineImpactBindingVo> botBindings = listBotsByWorkflowId(workflowId);
List<OfflineImpactBindingVo> pluginBindings = workflowPluginDependencyService.listPluginsByWorkflowId(workflowId);
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
result.setCanProceed(true);
result.setBotBindings(botBindings);
result.setHasBotBindings(!botBindings.isEmpty());
result.setPluginBindings(pluginBindings);
result.setHasPluginBindings(!pluginBindings.isEmpty());
result.setWorkflowUsages(Collections.emptyList());
result.setHasWorkflowUsages(false);
result.setMessage(botBindings.isEmpty()
? "当前工作流下线后不会影响已有绑定"
: "当前工作流下线成功后,将自动从相关聊天助手中解绑");
result.setMessage(resolveWorkflowOfflineImpactMessage(botBindings, pluginBindings));
return result;
}
@@ -198,6 +203,20 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
return result;
}
private String resolveWorkflowOfflineImpactMessage(List<OfflineImpactBindingVo> botBindings,
List<OfflineImpactBindingVo> pluginBindings) {
if (!pluginBindings.isEmpty() && !botBindings.isEmpty()) {
return "当前工作流被插件和聊天助手引用,下线后插件将不可用,聊天助手将自动解绑";
}
if (!pluginBindings.isEmpty()) {
return "当前工作流被插件引用,下线后相关插件将不可用";
}
if (!botBindings.isEmpty()) {
return "当前工作流下线成功后,将自动从相关聊天助手中解绑";
}
return "当前工作流下线后不会影响已有绑定";
}
private boolean containsKnowledgeReference(String content, BigInteger knowledgeId) {
if (!StringUtils.hasText(content) || knowledgeId == null) {
return false;

View File

@@ -14,10 +14,14 @@ public class OfflineImpactCheckVo {
private boolean hasWorkflowUsages;
private boolean hasPluginBindings;
private List<OfflineImpactBindingVo> botBindings = new ArrayList<>();
private List<OfflineImpactBindingVo> workflowUsages = new ArrayList<>();
private List<OfflineImpactBindingVo> pluginBindings = new ArrayList<>();
private String message;
/**
@@ -110,6 +114,22 @@ public class OfflineImpactCheckVo {
this.workflowUsages = workflowUsages;
}
public boolean isHasPluginBindings() {
return hasPluginBindings;
}
public void setHasPluginBindings(boolean hasPluginBindings) {
this.hasPluginBindings = hasPluginBindings;
}
public List<OfflineImpactBindingVo> getPluginBindings() {
return pluginBindings;
}
public void setPluginBindings(List<OfflineImpactBindingVo> pluginBindings) {
this.pluginBindings = pluginBindings;
}
/**
* 获取提示信息。
*

View File

@@ -0,0 +1,252 @@
package tech.easyflow.ai.plugin.workflow.dependency;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.mapper.PluginItemMapper;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.service.WorkflowService;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
public class WorkflowPluginDependencyServiceImplTest {
@Test
public void testPublishedCheckShouldIgnoreUnpublishedDraftCycle() {
WorkflowPluginDependencyServiceImpl service = newService(
workflows(
workflowVariant(rootWorkflowContent("2"), rootWorkflowContent("2")),
workflowVariant(pluginWorkflowContent("700"), terminalWorkflowContent())
),
plugins(),
pluginItems()
);
Assert.assertTrue(service.containsPluginReferenceTransitively(BigInteger.ONE, BigInteger.valueOf(900)));
Assert.assertFalse(service.containsPluginReferenceTransitivelyInPublishedSnapshot(BigInteger.ONE, BigInteger.valueOf(900)));
}
@Test
public void testPublishedCheckShouldBlockPublishedCycleEvenWhenDraftClean() {
WorkflowPluginDependencyServiceImpl service = newService(
workflows(
workflowVariant(rootWorkflowContent("2"), rootWorkflowContent("2")),
workflowVariant(terminalWorkflowContent(), pluginWorkflowContent("700"))
),
plugins(),
pluginItems()
);
Assert.assertFalse(service.containsPluginReferenceTransitively(BigInteger.ONE, BigInteger.valueOf(900)));
Assert.assertTrue(service.containsPluginReferenceTransitivelyInPublishedSnapshot(BigInteger.ONE, BigInteger.valueOf(900)));
}
private static WorkflowPluginDependencyServiceImpl newService(Map<String, WorkflowVariant> workflowStore,
Map<String, Plugin> pluginStore,
Map<String, PluginItem> pluginItemStore) {
return new WorkflowPluginDependencyServiceImpl(
mockPluginMapper(pluginStore),
mockPluginItemMapper(pluginItemStore),
mockWorkflowService(workflowStore)
);
}
private static Map<String, WorkflowVariant> workflows(WorkflowVariant root, WorkflowVariant child) {
Map<String, WorkflowVariant> workflows = new HashMap<>();
workflows.put("1", root);
workflows.put("2", child);
return workflows;
}
private static Map<String, Plugin> plugins() {
Map<String, Plugin> plugins = new HashMap<>();
Plugin plugin = new Plugin();
plugin.setId(BigInteger.valueOf(900));
plugin.setType(PluginType.WORKFLOW.getCode());
plugin.setWorkflowId(BigInteger.valueOf(30));
plugins.put("900", plugin);
return plugins;
}
private static Map<String, PluginItem> pluginItems() {
Map<String, PluginItem> pluginItems = new HashMap<>();
PluginItem pluginItem = new PluginItem();
pluginItem.setId(BigInteger.valueOf(700));
pluginItem.setPluginId(BigInteger.valueOf(900));
pluginItems.put("700", pluginItem);
return pluginItems;
}
private static WorkflowService mockWorkflowService(Map<String, WorkflowVariant> workflowStore) {
return (WorkflowService) Proxy.newProxyInstance(
WorkflowService.class.getClassLoader(),
new Class[]{WorkflowService.class},
(proxy, method, args) -> {
String methodName = method.getName();
if ("getById".equals(methodName)) {
return buildWorkflow(workflowStore, args == null ? null : args[0], false);
}
if ("getPublishedById".equals(methodName)) {
return buildWorkflow(workflowStore, args == null ? null : args[0], true);
}
if ("equals".equals(methodName)) {
return proxy == args[0];
}
if ("hashCode".equals(methodName)) {
return System.identityHashCode(proxy);
}
if (method.getReturnType() == boolean.class) {
return false;
}
if (method.getReturnType() == int.class) {
return 0;
}
if (method.getReturnType() == long.class) {
return 0L;
}
return null;
}
);
}
private static PluginMapper mockPluginMapper(Map<String, Plugin> pluginStore) {
return (PluginMapper) Proxy.newProxyInstance(
PluginMapper.class.getClassLoader(),
new Class[]{PluginMapper.class},
(proxy, method, args) -> {
if ("selectOneById".equals(method.getName())) {
return pluginStore.get(String.valueOf(args[0]));
}
if ("equals".equals(method.getName())) {
return proxy == args[0];
}
if ("hashCode".equals(method.getName())) {
return System.identityHashCode(proxy);
}
if (method.getReturnType() == boolean.class) {
return false;
}
if (method.getReturnType() == int.class) {
return 0;
}
if (method.getReturnType() == long.class) {
return 0L;
}
return null;
}
);
}
private static PluginItemMapper mockPluginItemMapper(Map<String, PluginItem> pluginItemStore) {
return (PluginItemMapper) Proxy.newProxyInstance(
PluginItemMapper.class.getClassLoader(),
new Class[]{PluginItemMapper.class},
(proxy, method, args) -> {
if ("selectOneById".equals(method.getName())) {
return pluginItemStore.get(String.valueOf(args[0]));
}
if ("equals".equals(method.getName())) {
return proxy == args[0];
}
if ("hashCode".equals(method.getName())) {
return System.identityHashCode(proxy);
}
if (method.getReturnType() == boolean.class) {
return false;
}
if (method.getReturnType() == int.class) {
return 0;
}
if (method.getReturnType() == long.class) {
return 0L;
}
return null;
}
);
}
private static Workflow buildWorkflow(Map<String, WorkflowVariant> workflowStore, Object idValue, boolean published) {
if (idValue == null) {
return null;
}
WorkflowVariant variant = workflowStore.get(String.valueOf(idValue));
if (variant == null) {
return null;
}
Workflow workflow = new Workflow();
try {
workflow.setId(new BigInteger(String.valueOf(idValue)));
} catch (Exception ignored) {
workflow.setId(null);
}
workflow.setContent(published ? variant.publishedContent : variant.draftContent);
return workflow;
}
private static WorkflowVariant workflowVariant(String draftContent, String publishedContent) {
WorkflowVariant variant = new WorkflowVariant();
variant.draftContent = draftContent;
variant.publishedContent = publishedContent;
return variant;
}
private static String rootWorkflowContent(String childWorkflowId) {
return workflowJson(array(workflowNode("wf-1", childWorkflowId)));
}
private static String pluginWorkflowContent(String pluginItemId) {
return workflowJson(array(pluginNode("plugin-1", pluginItemId)));
}
private static String terminalWorkflowContent() {
return workflowJson(new JSONArray());
}
private static String workflowJson(JSONArray nodes) {
JSONObject root = new JSONObject();
root.put("nodes", nodes);
root.put("edges", new JSONArray());
return root.toJSONString();
}
private static JSONArray array(JSONObject... objects) {
JSONArray array = new JSONArray();
for (JSONObject object : objects) {
array.add(object);
}
return array;
}
private static JSONObject workflowNode(String id, String workflowId) {
JSONObject data = new JSONObject();
data.put("workflowId", workflowId);
return node(id, "workflow-node", data);
}
private static JSONObject pluginNode(String id, String pluginItemId) {
JSONObject data = new JSONObject();
data.put("pluginId", pluginItemId);
return node(id, "plugin-node", data);
}
private static JSONObject node(String id, String type, JSONObject data) {
JSONObject node = new JSONObject();
node.put("id", id);
node.put("type", type);
node.put("data", data);
return node;
}
private static class WorkflowVariant {
private String draftContent;
private String publishedContent;
}
}

View File

@@ -1,5 +1,6 @@
package tech.easyflow.system.service;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import java.math.BigInteger;
@@ -9,11 +10,17 @@ public interface CategoryPermissionService {
boolean isCurrentSuperAdmin();
boolean isSuperAdmin(LoginAccount loginAccount);
RoleCategoryAccessSnapshot getCurrentAccess(String resourceType);
RoleCategoryAccessSnapshot getAccess(String resourceType, LoginAccount loginAccount);
Set<BigInteger> getCurrentVisibleCategoryIds(String resourceType);
boolean canAccessCategory(String resourceType, BigInteger createdBy, BigInteger categoryId);
boolean canAccessCategory(LoginAccount loginAccount, String resourceType, BigInteger createdBy, BigInteger categoryId);
void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message);
}

View File

@@ -1,5 +1,6 @@
package tech.easyflow.system.service;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.permission.resource.VisibilityResource;
@@ -8,5 +9,7 @@ public interface ResourceAccessService {
boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action);
boolean canAccess(LoginAccount loginAccount, CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action);
void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message);
}

View File

@@ -44,6 +44,11 @@ public class CategoryPermissionServiceImpl implements CategoryPermissionService
return false;
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
return isSuperAdmin(loginAccount);
}
@Override
public boolean isSuperAdmin(LoginAccount loginAccount) {
return loginAccount != null && isSuperAdmin(loginAccount.getId());
}
@@ -53,6 +58,11 @@ public class CategoryPermissionServiceImpl implements CategoryPermissionService
return new RoleCategoryAccessSnapshot(resourceType, null, false, true, Collections.emptySet());
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
return getAccess(resourceType, loginAccount);
}
@Override
public RoleCategoryAccessSnapshot getAccess(String resourceType, LoginAccount loginAccount) {
if (loginAccount == null) {
return new RoleCategoryAccessSnapshot(resourceType, null, false, true, Collections.emptySet());
}
@@ -100,6 +110,12 @@ public class CategoryPermissionServiceImpl implements CategoryPermissionService
return snapshot.canAccess(createdBy, categoryId);
}
@Override
public boolean canAccessCategory(LoginAccount loginAccount, String resourceType, BigInteger createdBy, BigInteger categoryId) {
RoleCategoryAccessSnapshot snapshot = getAccess(resourceType, loginAccount);
return snapshot.canAccess(createdBy, categoryId);
}
@Override
public void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message) {
if (!canAccessCategory(resourceType, createdBy, categoryId)) {

View File

@@ -26,15 +26,19 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
@Override
public boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
return canAccess(SaTokenUtil.getLoginAccount(), resourceType, resource, action);
}
@Override
public boolean canAccess(LoginAccount loginAccount, CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
if (resource == null) {
return false;
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (loginAccount == null || loginAccount.getId() == null) {
return false;
}
BigInteger accountId = loginAccount.getId();
if (categoryPermissionService.isCurrentSuperAdmin()) {
if (categoryPermissionService.isSuperAdmin(loginAccount)) {
return true;
}
if (accountId.equals(resource.getCreatedBy())) {
@@ -43,7 +47,7 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
if (ResourceAction.MANAGE == action) {
return false;
}
if (!categoryPermissionService.canAccessCategory(resourceType.getCode(), resource.getCreatedBy(), resource.getCategoryId())) {
if (!categoryPermissionService.canAccessCategory(loginAccount, resourceType.getCode(), resource.getCreatedBy(), resource.getCategoryId())) {
return false;
}
VisibilityScope scope = VisibilityScope.fromOrDefault(resource.getVisibilityScope(), VisibilityScope.PRIVATE);