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

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

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

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

View File

@@ -4,6 +4,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.BotPlugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.tree.Tree;
@@ -58,7 +59,14 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
List<BotPlugin> visibleList = new ArrayList<>();
for (BotPlugin relation : botPlugins) {
Plugin plugin = relation.getAiPlugin();
if (plugin == null || pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
if (plugin == null) {
visibleList.add(relation);
continue;
}
if (PluginType.isWorkflow(plugin.getType())) {
continue;
}
if (pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
visibleList.add(relation);
}
}
@@ -73,7 +81,13 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
List<Plugin> plugins = botPluginService.getList(botId);
List<Plugin> visibleList = new ArrayList<>();
for (Plugin plugin : plugins) {
if (plugin == null || pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
if (plugin == null) {
continue;
}
if (PluginType.isWorkflow(plugin.getType())) {
continue;
}
if (pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
visibleList.add(plugin);
}
}
@@ -105,6 +119,9 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
if (pluginItem.getPluginId() != null) {
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
if (plugin != null) {
if (PluginType.isWorkflow(plugin.getType())) {
throw new tech.easyflow.common.web.exceptions.BusinessException("当前版本暂不支持聊天助手绑定工作流插件");
}
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定插件");
}
}

View File

@@ -1,9 +1,11 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.ai.entity.PluginCategory;
import tech.easyflow.ai.entity.PluginCategoryMapping;
import tech.easyflow.ai.service.PluginCategoryMappingService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
@@ -21,6 +23,7 @@ import java.util.List;
*/
@RestController
@RequestMapping("/api/v1/pluginCategoryMapping")
@UsePermission(moduleName = "/api/v1/plugin")
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
public PluginCategoryMappingController(PluginCategoryMappingService service) {
super(service);
@@ -30,6 +33,7 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
private PluginCategoryMappingService relationService;
@PostMapping("/updateRelation")
@SaCheckPermission("/api/v1/plugin/save")
public Result<Boolean> updateRelation(
@JsonBody(value="pluginId") BigInteger pluginId,
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
@@ -42,4 +46,4 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
){
return Result.ok(relationService.getPluginCategories(pluginId));
}
}
}

View File

@@ -8,19 +8,30 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.ai.service.PluginService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -47,6 +58,14 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
private PluginVisibilityService pluginVisibilityService;
@Resource
private ModelService modelService;
@Resource
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
@Resource
private WorkflowService workflowService;
@Resource
private ResourceAccessService resourceAccessService;
@Resource
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
@Override
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
@@ -79,7 +98,8 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
public Result<List<Plugin>> getList(){
QueryWrapper queryWrapper = QueryWrapper.create().select();
applyCategoryPermission(queryWrapper);
return Result.ok(service.getMapper().selectListByQuery(queryWrapper));
List<Plugin> plugins = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
return Result.ok(pluginService.preparePluginsForCurrentUser(plugins, true, false));
}
@GetMapping("/pageByCategory")
@@ -101,6 +121,23 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
}
}
@GetMapping("/workflowCandidates")
@SaCheckPermission("/api/v1/plugin/query")
public Result<List<Workflow>> workflowCandidates(String keyword) {
QueryWrapper queryWrapper = QueryWrapper.create();
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
queryWrapper.eq("publish_status", tech.easyflow.ai.enums.PublishStatus.PUBLISHED.getCode());
if (keyword != null && !keyword.isBlank()) {
queryWrapper.like("title", keyword.trim());
}
queryWrapper.orderBy("modified desc");
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
List<Workflow> workflows = workflowService.list(queryWrapper).stream()
.filter(workflow -> canBindWorkflowCandidate(workflow, loginAccount))
.collect(java.util.stream.Collectors.toCollection(ArrayList::new));
return Result.ok(workflows);
}
@GetMapping("/modelList")
@SaCheckPermission("/api/v1/plugin/query")
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
@@ -110,14 +147,24 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
@Override
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
applyCategoryPermission(queryWrapper);
return service.getMapper().paginateWithRelations(page, queryWrapper);
List<Plugin> totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
boolean availableOnly = isAvailableOnly();
List<Plugin> prepared = pluginService.preparePluginsForCurrentUser(totalList, !availableOnly, availableOnly);
long total = prepared.size();
int fromIndex = Math.max(0, Math.toIntExact((page.getPageNumber() - 1) * page.getPageSize()));
if (fromIndex >= prepared.size()) {
return new Page<>(Collections.emptyList(), page.getPageNumber(), page.getPageSize(), total);
}
int toIndex = Math.min(prepared.size(), Math.toIntExact(fromIndex + page.getPageSize()));
return new Page<>(prepared.subList(fromIndex, toIndex), page.getPageNumber(), page.getPageSize(), total);
}
@Override
public Result<Plugin> detail(String id) {
Plugin plugin = service.getById(id);
Plugin plugin = service.getMapper().selectOneWithRelationsById(id);
if (plugin != null) {
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
pluginService.preparePluginForCurrentUser(plugin);
}
return Result.ok(plugin);
}
@@ -138,4 +185,30 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
}
queryWrapper.and(PLUGIN.CREATED_BY.eq(access.getAccountIdAsLong()).or(PLUGIN.ID.in(pluginIds)));
}
private boolean isAvailableOnly() {
HttpServletRequest request = currentHttpRequest();
return request != null && "true".equalsIgnoreCase(request.getParameter("availableOnly"));
}
private HttpServletRequest currentHttpRequest() {
org.springframework.web.context.request.ServletRequestAttributes attributes =
(org.springframework.web.context.request.ServletRequestAttributes)
org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
return attributes == null ? null : attributes.getRequest();
}
private boolean canBindWorkflowCandidate(Workflow workflow, LoginAccount loginAccount) {
if (workflow == null || loginAccount == null || loginAccount.getId() == null) {
return false;
}
if (workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
return false;
}
Workflow publishedWorkflow = workflowService.toPublishedView(workflow);
if (!workflowPluginSnapshotResolver.isSupportedForWorkflowPlugin(publishedWorkflow)) {
return false;
}
return resourceAccessService.canAccess(loginAccount, CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE);
}
}

View File

@@ -1,30 +1,49 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.BotPlugin;
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.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.service.BotPluginService;
import tech.easyflow.ai.service.PluginService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 控制层。
@@ -45,6 +64,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
@Resource
private BotPluginService botPluginService;
@Resource
private PluginService pluginService;
@Resource
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
@Resource
private WorkflowService workflowService;
@Resource
private ChainExecutor chainExecutor;
@Resource
private TinyFlowService tinyFlowService;
@Resource
private WorkflowCheckService workflowCheckService;
@PostMapping("/tool/save")
@SaCheckPermission("/api/v1/plugin/save")
@@ -87,8 +118,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
if (record == null) {
return Result.ok(nodeData);
}
Plugin plugin = pluginService.getById(record.getPluginId());
nodeData.put("pluginId", record.getId().toString());
nodeData.put("pluginName", record.getName());
if (plugin != null) {
plugin = pluginService.preparePluginForCurrentUser(plugin);
nodeData.put("pluginType", plugin.getType());
nodeData.put("workflowId", plugin.getWorkflowId());
nodeData.put("workflowTitle", plugin.getWorkflowTitle());
nodeData.put("available", plugin.getAvailable());
nodeData.put("reasonCode", plugin.getReasonCode());
nodeData.put("reasonMessage", plugin.getReasonMessage());
}
JSONArray parameters = new JSONArray();
JSONArray outputDefs = new JSONArray();
@@ -104,6 +145,7 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
handleArray(array);
outputDefs = array;
}
nodeData.put("schemaHash", resolveSchemaHash(record, plugin));
nodeData.put("parameters", parameters);
nodeData.put("outputDefs", outputDefs);
return Result.ok(nodeData);
@@ -119,6 +161,71 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
return pluginItemService.pluginToolTest(inputData, pluginToolId);
}
/**
* 工作流插件异步试运行。
*
* @param inputData 输入参数 JSON
* @param pluginToolId 插件工具 ID
* @return 执行 ID
*/
@PostMapping("/testAsync")
@SaCheckPermission("/api/v1/plugin/query")
public Result<String> pluginToolTestAsync(@JsonBody(value = "inputData", required = true) String inputData,
@JsonBody(value = "pluginToolId", required = true) BigInteger pluginToolId) {
PluginItem pluginItem = pluginItemService.getById(pluginToolId);
Plugin plugin = requireWorkflowPlugin(pluginItem);
Plugin preparedPlugin = pluginService.preparePluginForCurrentUser(plugin);
if (Boolean.FALSE.equals(preparedPlugin.getAvailable())) {
throw new BusinessException(preparedPlugin.getReasonMessage());
}
Workflow workflow = workflowService.getPublishedById(preparedPlugin.getWorkflowId());
if (workflow == null) {
throw new BusinessException("未找到已发布工作流");
}
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
Map<String, Object> variables = JSON.parseObject(inputData, Map.class);
if (variables == null) {
variables = new HashMap<>();
}
if (StpUtil.isLogin()) {
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
}
String executeId = chainExecutor.executeAsync(
PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId())),
variables
);
return Result.ok(executeId);
}
/**
* 获取工作流插件试运行状态。
*
* @param executeId 执行 ID
* @param nodes 节点列表
* @return 链路状态
*/
@PostMapping("/testChainStatus")
@SaCheckPermission("/api/v1/plugin/query")
public Result<ChainInfo> pluginToolTestChainStatus(@JsonBody(value = "executeId", required = true) String executeId,
@JsonBody("nodes") List<NodeInfo> nodes) {
return Result.ok(tinyFlowService.getChainStatus(executeId, nodes));
}
/**
* 恢复工作流插件试运行。
*
* @param executeId 执行 ID
* @param confirmParams 恢复参数
* @return 空结果
*/
@PostMapping("/testResume")
@SaCheckPermission("/api/v1/plugin/query")
public Result<Void> pluginToolTestResume(@JsonBody(value = "executeId", required = true) String executeId,
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
chainExecutor.resumeAsync(executeId, confirmParams);
return Result.ok();
}
private void handleArray(JSONArray array) {
for (Object o : array) {
JSONObject obj = (JSONObject) o;
@@ -134,6 +241,40 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
}
}
private String resolveSchemaHash(PluginItem record, Plugin plugin) {
if (record == null) {
return null;
}
if (StrUtil.isNotBlank(record.getSchemaHash())) {
return record.getSchemaHash();
}
if (plugin == null || !PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
return null;
}
tech.easyflow.ai.entity.Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
if (workflow == null) {
return null;
}
return workflowPluginSnapshotResolver.resolveSchemaHash(workflow);
}
private Plugin requireWorkflowPlugin(PluginItem pluginItem) {
if (pluginItem == null) {
throw new BusinessException("插件工具不存在");
}
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
if (plugin == null) {
throw new BusinessException("插件不存在");
}
if (!PluginType.isWorkflow(plugin.getType())) {
throw new BusinessException("当前工具不是工作流插件");
}
if (plugin.getWorkflowId() == null) {
throw new BusinessException("插件未绑定工作流");
}
return plugin;
}
@Override
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
@@ -144,6 +285,15 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
if (exists){
return Result.fail(1, "此工具还关联着bot请先取消关联");
}
if (ids.size() == 1) {
PluginItem pluginItem = pluginItemService.getById(ids.iterator().next());
if (pluginItem != null) {
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
return Result.fail(1, "工作流插件工具由系统自动维护,不支持删除");
}
}
}
return null;
}

View File

@@ -120,6 +120,12 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
if (workflow == null) {
return Result.fail(1, "工作流不存在");
}
if (variables == null) {
variables = new HashMap<>();
}
if (StpUtil.isLogin()) {
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
}
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
return Result.ok(res);
}

View File

@@ -1,6 +1,7 @@
package tech.easyflow.publicapi.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
@@ -18,7 +19,9 @@ import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.jsonbody.JsonBody;
import java.math.BigInteger;
@@ -75,6 +78,12 @@ public class PublicWorkflowController {
if (workflow == null) {
return Result.fail(1, "工作流不存在");
}
if (variables == null) {
variables = new HashMap<>();
}
if (StpUtil.isLogin()) {
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
}
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
return Result.ok(res);
}

View File

@@ -83,6 +83,12 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
if (workflow == null) {
return Result.fail(1, "工作流不存在");
}
if (variables == null) {
variables = new HashMap<>();
}
if (StpUtil.isLogin()) {
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
}
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
return Result.ok(res);
}