feat: 支持工作流插件复用与试运行
- 新增工作流插件类型、发布快照同步、实时可用性与下线影响检查 - 收口绑定候选、分类权限、间接环路校验与运行态优雅降级 - 补齐管理端工作流插件配置、详情与试运行界面及定向测试
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提示信息。
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user