feat: 支持工作流插件复用与试运行
- 新增工作流插件类型、发布快照同步、实时可用性与下线影响检查 - 收口绑定候选、分类权限、间接环路校验与运行态优雅降级 - 补齐管理端工作流插件配置、详情与试运行界面及定向测试
This commit is contained in:
@@ -4,6 +4,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import tech.easyflow.ai.entity.Plugin;
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.BotPlugin;
|
import tech.easyflow.ai.entity.BotPlugin;
|
||||||
import tech.easyflow.ai.entity.PluginItem;
|
import tech.easyflow.ai.entity.PluginItem;
|
||||||
|
import tech.easyflow.ai.enums.PluginType;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.tree.Tree;
|
import tech.easyflow.common.tree.Tree;
|
||||||
@@ -58,7 +59,14 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
List<BotPlugin> visibleList = new ArrayList<>();
|
List<BotPlugin> visibleList = new ArrayList<>();
|
||||||
for (BotPlugin relation : botPlugins) {
|
for (BotPlugin relation : botPlugins) {
|
||||||
Plugin plugin = relation.getAiPlugin();
|
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);
|
visibleList.add(relation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +81,13 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
List<Plugin> plugins = botPluginService.getList(botId);
|
List<Plugin> plugins = botPluginService.getList(botId);
|
||||||
List<Plugin> visibleList = new ArrayList<>();
|
List<Plugin> visibleList = new ArrayList<>();
|
||||||
for (Plugin plugin : plugins) {
|
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);
|
visibleList.add(plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +119,9 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
if (pluginItem.getPluginId() != null) {
|
if (pluginItem.getPluginId() != null) {
|
||||||
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||||
if (plugin != null) {
|
if (plugin != null) {
|
||||||
|
if (PluginType.isWorkflow(plugin.getType())) {
|
||||||
|
throw new tech.easyflow.common.web.exceptions.BusinessException("当前版本暂不支持聊天助手绑定工作流插件");
|
||||||
|
}
|
||||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定插件");
|
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定插件");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import tech.easyflow.ai.entity.PluginCategory;
|
import tech.easyflow.ai.entity.PluginCategory;
|
||||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||||
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
||||||
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
@@ -21,6 +23,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/pluginCategoryMapping")
|
@RequestMapping("/api/v1/pluginCategoryMapping")
|
||||||
|
@UsePermission(moduleName = "/api/v1/plugin")
|
||||||
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
|
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
|
||||||
public PluginCategoryMappingController(PluginCategoryMappingService service) {
|
public PluginCategoryMappingController(PluginCategoryMappingService service) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -30,6 +33,7 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
|
|||||||
private PluginCategoryMappingService relationService;
|
private PluginCategoryMappingService relationService;
|
||||||
|
|
||||||
@PostMapping("/updateRelation")
|
@PostMapping("/updateRelation")
|
||||||
|
@SaCheckPermission("/api/v1/plugin/save")
|
||||||
public Result<Boolean> updateRelation(
|
public Result<Boolean> updateRelation(
|
||||||
@JsonBody(value="pluginId") BigInteger pluginId,
|
@JsonBody(value="pluginId") BigInteger pluginId,
|
||||||
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
|
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
|
||||||
|
|||||||
@@ -8,19 +8,30 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import tech.easyflow.ai.entity.Model;
|
import tech.easyflow.ai.entity.Model;
|
||||||
import tech.easyflow.ai.entity.Plugin;
|
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.ModelService;
|
||||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
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.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.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.ai.service.PluginService;
|
import tech.easyflow.ai.service.PluginService;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
import tech.easyflow.system.service.CategoryPermissionService;
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -47,6 +58,14 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
private PluginVisibilityService pluginVisibilityService;
|
private PluginVisibilityService pluginVisibilityService;
|
||||||
@Resource
|
@Resource
|
||||||
private ModelService modelService;
|
private ModelService modelService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private WorkflowService workflowService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
||||||
@@ -79,7 +98,8 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
public Result<List<Plugin>> getList(){
|
public Result<List<Plugin>> getList(){
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().select();
|
QueryWrapper queryWrapper = QueryWrapper.create().select();
|
||||||
applyCategoryPermission(queryWrapper);
|
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")
|
@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")
|
@GetMapping("/modelList")
|
||||||
@SaCheckPermission("/api/v1/plugin/query")
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
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
|
@Override
|
||||||
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
||||||
applyCategoryPermission(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
|
@Override
|
||||||
public Result<Plugin> detail(String id) {
|
public Result<Plugin> detail(String id) {
|
||||||
Plugin plugin = service.getById(id);
|
Plugin plugin = service.getMapper().selectOneWithRelationsById(id);
|
||||||
if (plugin != null) {
|
if (plugin != null) {
|
||||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
||||||
|
pluginService.preparePluginForCurrentUser(plugin);
|
||||||
}
|
}
|
||||||
return Result.ok(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)));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,49 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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.BotPlugin;
|
||||||
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.PluginItem;
|
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.BotPluginService;
|
||||||
|
import tech.easyflow.ai.service.PluginService;
|
||||||
import tech.easyflow.ai.service.PluginItemService;
|
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.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
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.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
@@ -45,6 +64,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BotPluginService botPluginService;
|
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")
|
@PostMapping("/tool/save")
|
||||||
@SaCheckPermission("/api/v1/plugin/save")
|
@SaCheckPermission("/api/v1/plugin/save")
|
||||||
@@ -87,8 +118,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
if (record == null) {
|
if (record == null) {
|
||||||
return Result.ok(nodeData);
|
return Result.ok(nodeData);
|
||||||
}
|
}
|
||||||
|
Plugin plugin = pluginService.getById(record.getPluginId());
|
||||||
nodeData.put("pluginId", record.getId().toString());
|
nodeData.put("pluginId", record.getId().toString());
|
||||||
nodeData.put("pluginName", record.getName());
|
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 parameters = new JSONArray();
|
||||||
JSONArray outputDefs = new JSONArray();
|
JSONArray outputDefs = new JSONArray();
|
||||||
@@ -104,6 +145,7 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
handleArray(array);
|
handleArray(array);
|
||||||
outputDefs = array;
|
outputDefs = array;
|
||||||
}
|
}
|
||||||
|
nodeData.put("schemaHash", resolveSchemaHash(record, plugin));
|
||||||
nodeData.put("parameters", parameters);
|
nodeData.put("parameters", parameters);
|
||||||
nodeData.put("outputDefs", outputDefs);
|
nodeData.put("outputDefs", outputDefs);
|
||||||
return Result.ok(nodeData);
|
return Result.ok(nodeData);
|
||||||
@@ -119,6 +161,71 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
return pluginItemService.pluginToolTest(inputData, pluginToolId);
|
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) {
|
private void handleArray(JSONArray array) {
|
||||||
for (Object o : array) {
|
for (Object o : array) {
|
||||||
JSONObject obj = (JSONObject) o;
|
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
|
@Override
|
||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
|
||||||
@@ -144,6 +285,15 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
if (exists){
|
if (exists){
|
||||||
return Result.fail(1, "此工具还关联着bot,请先取消关联!");
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
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);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package tech.easyflow.publicapi.controller;
|
package tech.easyflow.publicapi.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
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.ChainDefinition;
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
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.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.common.constant.Constants;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -75,6 +78,12 @@ public class PublicWorkflowController {
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
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);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
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);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.entity.WorkflowExecResult;
|
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||||
import tech.easyflow.ai.entity.WorkflowExecStep;
|
import tech.easyflow.ai.entity.WorkflowExecStep;
|
||||||
@@ -62,7 +63,11 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
|||||||
log.info("ChainStartEvent: {}", event);
|
log.info("ChainStartEvent: {}", event);
|
||||||
ChainDefinition definition = chain.getDefinition();
|
ChainDefinition definition = chain.getDefinition();
|
||||||
ChainState state = chain.getState();
|
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();
|
String instanceId = state.getInstanceId();
|
||||||
WorkflowExecResult existed = workflowExecResultService.getByExecKey(instanceId);
|
WorkflowExecResult existed = workflowExecResultService.getByExecKey(instanceId);
|
||||||
if (existed != null) {
|
if (existed != null) {
|
||||||
@@ -176,4 +181,26 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
|||||||
ChainState chainState = chain.getChainStateRepository().load(parentInstanceId);
|
ChainState chainState = chain.getChainStateRepository().load(parentInstanceId);
|
||||||
return findAncestorState(chainState, chain);
|
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.WorkflowCheckIssue;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||||
|
import tech.easyflow.ai.entity.PluginItem;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
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.ai.service.WorkflowService;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;
|
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_END = "endNode";
|
||||||
private static final String TYPE_LOOP = "loopNode";
|
private static final String TYPE_LOOP = "loopNode";
|
||||||
private static final String TYPE_WORKFLOW = "workflow-node";
|
private static final String TYPE_WORKFLOW = "workflow-node";
|
||||||
|
private static final String TYPE_PLUGIN = "plugin-node";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowService workflowService;
|
private WorkflowService workflowService;
|
||||||
@@ -47,6 +52,12 @@ public class WorkflowCheckService {
|
|||||||
private ChainParser chainParser;
|
private ChainParser chainParser;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginDependencyService workflowPluginDependencyService;
|
||||||
|
@Resource
|
||||||
|
private PluginItemService pluginItemService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
|
||||||
public WorkflowCheckResult checkWorkflow(BigInteger workflowId, WorkflowCheckStage stage) {
|
public WorkflowCheckResult checkWorkflow(BigInteger workflowId, WorkflowCheckStage stage) {
|
||||||
if (workflowId == null) {
|
if (workflowId == null) {
|
||||||
@@ -66,6 +77,9 @@ public class WorkflowCheckService {
|
|||||||
List<WorkflowCheckIssue> issues = new ArrayList<>();
|
List<WorkflowCheckIssue> issues = new ArrayList<>();
|
||||||
Set<String> issueKeys = new LinkedHashSet<>();
|
Set<String> issueKeys = new LinkedHashSet<>();
|
||||||
ParsedWorkflow parsedWorkflow = parseAndCheckBase(content, issues, issueKeys);
|
ParsedWorkflow parsedWorkflow = parseAndCheckBase(content, issues, issueKeys);
|
||||||
|
if (parsedWorkflow != null) {
|
||||||
|
checkPluginSchemaHashes(parsedWorkflow, issues, issueKeys);
|
||||||
|
}
|
||||||
|
|
||||||
if (stage == WorkflowCheckStage.PRE_EXECUTE && parsedWorkflow != null) {
|
if (stage == WorkflowCheckStage.PRE_EXECUTE && parsedWorkflow != null) {
|
||||||
runStrictChecks(content, parsedWorkflow, currentWorkflowId, issues, issueKeys);
|
runStrictChecks(content, parsedWorkflow, currentWorkflowId, issues, issueKeys);
|
||||||
@@ -394,6 +408,10 @@ public class WorkflowCheckService {
|
|||||||
|
|
||||||
for (NodeView node : parsed.nodes) {
|
for (NodeView node : parsed.nodes) {
|
||||||
if (!TYPE_WORKFLOW.equals(node.type)) {
|
if (!TYPE_WORKFLOW.equals(node.type)) {
|
||||||
|
if (!TYPE_PLUGIN.equals(node.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
checkPluginWorkflowReference(node, currentWorkflowIdString, currentContent, contentCache, issues, issueKeys);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String workflowId = getWorkflowIdInNode(node);
|
String workflowId = getWorkflowIdInNode(node);
|
||||||
@@ -510,12 +528,86 @@ public class WorkflowCheckService {
|
|||||||
refs.add(workflowId);
|
refs.add(workflowId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refs.addAll(workflowPluginDependencyService.extractWorkflowIdsFromPluginNodes(content));
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
return refs;
|
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) {
|
private String formatCyclePath(LinkedHashSet<String> visiting, String cycleStart) {
|
||||||
List<String> chain = new ArrayList<>();
|
List<String> chain = new ArrayList<>();
|
||||||
boolean started = false;
|
boolean started = false;
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ public class Plugin extends PluginBase {
|
|||||||
@RelationOneToMany(selfField = "id", targetField = "pluginId", targetTable = "tb_plugin_item")
|
@RelationOneToMany(selfField = "id", targetField = "pluginId", targetTable = "tb_plugin_item")
|
||||||
private List<PluginItem> tools;
|
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() {
|
public String getTitle() {
|
||||||
return this.getName();
|
return this.getName();
|
||||||
}
|
}
|
||||||
@@ -30,4 +42,36 @@ public class Plugin extends PluginBase {
|
|||||||
public void setTools(List<PluginItem> tools) {
|
public void setTools(List<PluginItem> tools) {
|
||||||
this.tools = 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 = "类型")
|
@Column(comment = "类型")
|
||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定工作流ID
|
||||||
|
*/
|
||||||
|
@Column(comment = "绑定工作流ID")
|
||||||
|
private BigInteger workflowId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础URL
|
* 基础URL
|
||||||
*/
|
*/
|
||||||
@@ -148,6 +154,14 @@ public class PluginBase implements Serializable {
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getWorkflowId() {
|
||||||
|
return workflowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorkflowId(BigInteger workflowId) {
|
||||||
|
this.workflowId = workflowId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBaseUrl() {
|
public String getBaseUrl() {
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ public class PluginItemBase implements Serializable {
|
|||||||
@Column(comment = "英文名称")
|
@Column(comment = "英文名称")
|
||||||
private String englishName;
|
private String englishName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流插件输入输出契约哈希
|
||||||
|
*/
|
||||||
|
@Column(comment = "工作流插件输入输出契约哈希")
|
||||||
|
private String schemaHash;
|
||||||
|
|
||||||
public BigInteger getId() {
|
public BigInteger getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -194,4 +200,12 @@ public class PluginItemBase implements Serializable {
|
|||||||
this.englishName = englishName;
|
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.alibaba.fastjson.JSON;
|
||||||
import com.easyagents.flow.core.chain.Chain;
|
import com.easyagents.flow.core.chain.Chain;
|
||||||
import com.easyagents.flow.core.node.BaseNode;
|
import com.easyagents.flow.core.node.BaseNode;
|
||||||
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.PluginItem;
|
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.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 tech.easyflow.common.util.SpringContextUtil;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -32,6 +44,11 @@ public class PluginToolNode extends BaseNode {
|
|||||||
if (tool == null) {
|
if (tool == null) {
|
||||||
return Collections.emptyMap();
|
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();
|
Tool function = tool.toFunction();
|
||||||
if (function == null) {
|
if (function == null) {
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
@@ -49,6 +66,43 @@ public class PluginToolNode extends BaseNode {
|
|||||||
return JSON.parseObject(JSON.toJSONString(result), Map.class);
|
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() {
|
public BigInteger getPluginId() {
|
||||||
return pluginId;
|
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.BotWorkflow;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.enums.PublishStatus;
|
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.BotWorkflowService;
|
||||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
@@ -31,18 +33,24 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
private final ResourceAccessService resourceAccessService;
|
private final ResourceAccessService resourceAccessService;
|
||||||
private final BotWorkflowService botWorkflowService;
|
private final BotWorkflowService botWorkflowService;
|
||||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||||
|
private final WorkflowPluginBindingService workflowPluginBindingService;
|
||||||
|
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
|
||||||
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
||||||
ResourceAccessService resourceAccessService,
|
ResourceAccessService resourceAccessService,
|
||||||
ApprovalInstanceService approvalInstanceService,
|
ApprovalInstanceService approvalInstanceService,
|
||||||
BotWorkflowService botWorkflowService,
|
BotWorkflowService botWorkflowService,
|
||||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||||
|
WorkflowPluginBindingService workflowPluginBindingService,
|
||||||
|
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper) {
|
||||||
super(approvalInstanceService, objectMapper);
|
super(approvalInstanceService, objectMapper);
|
||||||
this.workflowService = workflowService;
|
this.workflowService = workflowService;
|
||||||
this.resourceAccessService = resourceAccessService;
|
this.resourceAccessService = resourceAccessService;
|
||||||
this.botWorkflowService = botWorkflowService;
|
this.botWorkflowService = botWorkflowService;
|
||||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||||
|
this.workflowPluginBindingService = workflowPluginBindingService;
|
||||||
|
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -116,6 +124,16 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
return snapshot;
|
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
|
@Override
|
||||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||||
Workflow update = new Workflow();
|
Workflow update = new Workflow();
|
||||||
@@ -135,6 +153,7 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
update.setPublishedAt(new java.util.Date());
|
update.setPublishedAt(new java.util.Date());
|
||||||
update.setPublishedBy(operatorId);
|
update.setPublishedBy(operatorId);
|
||||||
workflowService.updateById(update);
|
workflowService.updateById(update);
|
||||||
|
workflowPluginBindingService.syncByWorkflowId(resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -162,6 +181,9 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
if (impact.isHasBotBindings()) {
|
if (impact.isHasBotBindings()) {
|
||||||
snapshot.put("botBindings", impact.getBotBindings());
|
snapshot.put("botBindings", impact.getBotBindings());
|
||||||
}
|
}
|
||||||
|
if (impact.isHasPluginBindings()) {
|
||||||
|
snapshot.put("pluginBindings", impact.getPluginBindings());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -28,4 +28,12 @@ public interface PluginItemService extends IService<PluginItem> {
|
|||||||
Result pluginToolTest(String inputData, BigInteger pluginToolId);
|
Result pluginToolTest(String inputData, BigInteger pluginToolId);
|
||||||
|
|
||||||
List<PluginItem> getByPluginId(String id);
|
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);
|
Result pageByCategory(Long pageNumber, Long pageSize, int category);
|
||||||
|
|
||||||
boolean updatePlugin(Plugin plugin);
|
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.easyagents.tool.WorkflowTool;
|
||||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
|
import tech.easyflow.ai.enums.PluginType;
|
||||||
import tech.easyflow.ai.enums.PublishStatus;
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
import tech.easyflow.ai.mapper.BotMapper;
|
import tech.easyflow.ai.mapper.BotMapper;
|
||||||
import tech.easyflow.ai.service.*;
|
import tech.easyflow.ai.service.*;
|
||||||
@@ -117,6 +118,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
@Resource
|
@Resource
|
||||||
private BotPluginService botPluginService;
|
private BotPluginService botPluginService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private PluginService pluginService;
|
||||||
|
@Resource
|
||||||
private PluginItemService pluginItemService;
|
private PluginItemService pluginItemService;
|
||||||
@Resource
|
@Resource
|
||||||
private BotMcpService botMcpService;
|
private BotMcpService botMcpService;
|
||||||
@@ -508,6 +511,12 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
List<PluginItem> pluginItems = pluginItemService.getMapper().selectListWithRelationsByQuery(queryTool);
|
List<PluginItem> pluginItems = pluginItemService.getMapper().selectListWithRelationsByQuery(queryTool);
|
||||||
if (pluginItems != null && !pluginItems.isEmpty()) {
|
if (pluginItems != null && !pluginItems.isEmpty()) {
|
||||||
for (PluginItem pluginItem : pluginItems) {
|
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());
|
functionList.add(pluginItem.toFunction());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
package tech.easyflow.ai.service.impl;
|
package tech.easyflow.ai.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.PluginCategory;
|
import tech.easyflow.ai.entity.PluginCategory;
|
||||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||||
import tech.easyflow.ai.mapper.PluginCategoryMapper;
|
import tech.easyflow.ai.mapper.PluginCategoryMapper;
|
||||||
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
|
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
|
||||||
|
import tech.easyflow.ai.mapper.PluginMapper;
|
||||||
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
||||||
|
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
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 javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -34,6 +40,12 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private PluginCategoryMapper pluginCategoryMapper;
|
private PluginCategoryMapper pluginCategoryMapper;
|
||||||
|
@Resource
|
||||||
|
private PluginMapper pluginMapper;
|
||||||
|
@Resource
|
||||||
|
private PluginVisibilityService pluginVisibilityService;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -41,6 +53,11 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
|
|||||||
if (pluginId == null) {
|
if (pluginId == null) {
|
||||||
throw new BusinessException("插件ID不能为空");
|
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
|
List<BigInteger> targetCategoryIds = categoryIds == null
|
||||||
? Collections.emptyList()
|
? Collections.emptyList()
|
||||||
@@ -48,6 +65,8 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
|
|||||||
.filter(java.util.Objects::nonNull)
|
.filter(java.util.Objects::nonNull)
|
||||||
.distinct()
|
.distinct()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
assertCategoryIdsValid(targetCategoryIds);
|
||||||
|
assertCategoryAccess(targetCategoryIds);
|
||||||
|
|
||||||
QueryWrapper currentRelationQuery = QueryWrapper.create().select("category_id")
|
QueryWrapper currentRelationQuery = QueryWrapper.create().select("category_id")
|
||||||
.from("tb_plugin_category_mapping")
|
.from("tb_plugin_category_mapping")
|
||||||
@@ -80,6 +99,37 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
|
|||||||
return true;
|
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
|
@Override
|
||||||
public List<PluginCategory> getPluginCategories(BigInteger pluginId) {
|
public List<PluginCategory> getPluginCategories(BigInteger pluginId) {
|
||||||
QueryWrapper categoryQueryWrapper = QueryWrapper.create().select("category_id")
|
QueryWrapper categoryQueryWrapper = QueryWrapper.create().select("category_id")
|
||||||
|
|||||||
@@ -5,14 +5,22 @@ import com.mybatisflex.spring.service.impl.ServiceImpl;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import tech.easyflow.ai.entity.BotPlugin;
|
import tech.easyflow.ai.entity.BotPlugin;
|
||||||
import tech.easyflow.ai.entity.Plugin;
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.easyagents.tool.PluginTool;
|
|
||||||
import tech.easyflow.ai.entity.PluginItem;
|
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.BotPluginMapper;
|
||||||
import tech.easyflow.ai.mapper.PluginMapper;
|
import tech.easyflow.ai.mapper.PluginMapper;
|
||||||
import tech.easyflow.ai.mapper.PluginItemMapper;
|
import tech.easyflow.ai.mapper.PluginItemMapper;
|
||||||
import tech.easyflow.ai.service.PluginItemService;
|
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.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -37,9 +45,19 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BotPluginMapper botPluginMapper;
|
private BotPluginMapper botPluginMapper;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginAvailabilityService workflowPluginAvailabilityService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
@Resource
|
||||||
|
private WorkflowService workflowService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean savePluginTool(PluginItem pluginItem) {
|
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.setCreated(new Date());
|
||||||
pluginItem.setRequestMethod("Post");
|
pluginItem.setRequestMethod("Post");
|
||||||
int insert = pluginItemMapper.insert(pluginItem);
|
int insert = pluginItemMapper.insert(pluginItem);
|
||||||
@@ -61,14 +79,20 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
|
|||||||
.select()
|
.select()
|
||||||
.eq(Plugin::getId, pluginItem.getPluginId());
|
.eq(Plugin::getId, pluginItem.getPluginId());
|
||||||
Plugin plugin = pluginMapper.selectOneByQuery(queryAiPluginWrapper);
|
Plugin plugin = pluginMapper.selectOneByQuery(queryAiPluginWrapper);
|
||||||
|
plugin = preparePluginForCurrentUser(plugin);
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("data", pluginItem);
|
result.put("data", pluginItem);
|
||||||
result.put("aiPlugin", plugin);
|
result.put("aiPlugin", plugin);
|
||||||
|
result.put("workflowSnapshot", buildWorkflowSnapshot(plugin));
|
||||||
return Result.ok(result);
|
return Result.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean updatePlugin(PluginItem pluginItem) {
|
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);
|
int update = pluginItemMapper.update(pluginItem);
|
||||||
if (update <= 0) {
|
if (update <= 0) {
|
||||||
throw new BusinessException("修改失败");
|
throw new BusinessException("修改失败");
|
||||||
@@ -113,10 +137,37 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<?> pluginToolTest(String inputData, BigInteger pluginToolId) {
|
public Result<?> pluginToolTest(String inputData, BigInteger pluginToolId) {
|
||||||
PluginItem pluginItem = new PluginItem();
|
PluginItem pluginItem = pluginItemMapper.selectOneById(pluginToolId);
|
||||||
pluginItem.setId(pluginToolId);
|
if (pluginItem == null) {
|
||||||
pluginItem.setInputData(inputData);
|
throw new BusinessException("插件工具不存在");
|
||||||
PluginTool pluginTool = new PluginTool(pluginItem);
|
}
|
||||||
|
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));
|
return Result.ok(pluginTool.runPluginTool(null, inputData, pluginToolId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,4 +180,82 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
|
|||||||
return list(queryWrapper);
|
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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
|
import tech.easyflow.ai.enums.PluginType;
|
||||||
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
|
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
|
||||||
import tech.easyflow.ai.mapper.PluginMapper;
|
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.BotPluginService;
|
||||||
import tech.easyflow.ai.service.PluginItemService;
|
import tech.easyflow.ai.service.PluginItemService;
|
||||||
import tech.easyflow.ai.service.PluginService;
|
import tech.easyflow.ai.service.PluginService;
|
||||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
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.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
import tech.easyflow.system.service.CategoryPermissionService;
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
@@ -60,9 +66,18 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
|||||||
private CategoryPermissionService categoryPermissionService;
|
private CategoryPermissionService categoryPermissionService;
|
||||||
@Resource
|
@Resource
|
||||||
private PluginVisibilityService pluginVisibilityService;
|
private PluginVisibilityService pluginVisibilityService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginBindingService workflowPluginBindingService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginAvailabilityService workflowPluginAvailabilityService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Plugin savePlugin(Plugin plugin) {
|
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());
|
plugin.setCreated(new Date());
|
||||||
int insert = pluginMapper.insert(plugin);
|
int insert = pluginMapper.insert(plugin);
|
||||||
if (insert <= 0) {
|
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));
|
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));
|
int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize));
|
||||||
if (fromIndex >= visiblePluginIds.size()) {
|
if (fromIndex >= totalPlugins.size()) {
|
||||||
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, visiblePluginIds.size()));
|
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, totalPlugins.size()));
|
||||||
}
|
}
|
||||||
int toIndex = Math.min(visiblePluginIds.size(), Math.toIntExact(fromIndex + pageSize));
|
int toIndex = Math.min(totalPlugins.size(), Math.toIntExact(fromIndex + pageSize));
|
||||||
List<BigInteger> currentPagePluginIds = new ArrayList<>(visiblePluginIds.subList(fromIndex, toIndex));
|
Page<Plugin> aiPluginPage = new Page<>(new ArrayList<>(totalPlugins.subList(fromIndex, toIndex)),
|
||||||
List<Plugin> plugins = queryPluginsByIds(currentPagePluginIds);
|
pageNumber, pageSize, totalPlugins.size());
|
||||||
Page<Plugin> aiPluginPage = new Page<>(plugins, pageNumber, pageSize, visiblePluginIds.size());
|
|
||||||
return Result.ok(aiPluginPage);
|
return Result.ok(aiPluginPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean updatePlugin(Plugin plugin) {
|
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);
|
pluginMapper.update(plugin);
|
||||||
return true;
|
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) {
|
private List<BigInteger> queryCreatorPluginIds(List<BigInteger> pluginIds, Long creatorId) {
|
||||||
if (CollectionUtil.isEmpty(pluginIds) || creatorId == null) {
|
if (CollectionUtil.isEmpty(pluginIds) || creatorId == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
@@ -175,7 +256,7 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
|||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds);
|
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(
|
Map<BigInteger, Plugin> pluginMap = plugins.stream().collect(Collectors.toMap(
|
||||||
Plugin::getId,
|
Plugin::getId,
|
||||||
item -> item,
|
item -> item,
|
||||||
@@ -192,5 +273,24 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
|||||||
return orderedPlugins;
|
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.BotDocumentCollection;
|
||||||
import tech.easyflow.ai.entity.BotWorkflow;
|
import tech.easyflow.ai.entity.BotWorkflow;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
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.BotDocumentCollectionService;
|
||||||
import tech.easyflow.ai.service.BotService;
|
import tech.easyflow.ai.service.BotService;
|
||||||
import tech.easyflow.ai.service.BotWorkflowService;
|
import tech.easyflow.ai.service.BotWorkflowService;
|
||||||
@@ -49,17 +50,20 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
|
|||||||
private final BotService botService;
|
private final BotService botService;
|
||||||
private final WorkflowService workflowService;
|
private final WorkflowService workflowService;
|
||||||
private final RedisLockExecutor redisLockExecutor;
|
private final RedisLockExecutor redisLockExecutor;
|
||||||
|
private final WorkflowPluginDependencyService workflowPluginDependencyService;
|
||||||
|
|
||||||
public ResourceOfflineImpactServiceImpl(BotWorkflowService botWorkflowService,
|
public ResourceOfflineImpactServiceImpl(BotWorkflowService botWorkflowService,
|
||||||
BotDocumentCollectionService botDocumentCollectionService,
|
BotDocumentCollectionService botDocumentCollectionService,
|
||||||
BotService botService,
|
BotService botService,
|
||||||
WorkflowService workflowService,
|
WorkflowService workflowService,
|
||||||
RedisLockExecutor redisLockExecutor) {
|
RedisLockExecutor redisLockExecutor,
|
||||||
|
WorkflowPluginDependencyService workflowPluginDependencyService) {
|
||||||
this.botWorkflowService = botWorkflowService;
|
this.botWorkflowService = botWorkflowService;
|
||||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||||
this.botService = botService;
|
this.botService = botService;
|
||||||
this.workflowService = workflowService;
|
this.workflowService = workflowService;
|
||||||
this.redisLockExecutor = redisLockExecutor;
|
this.redisLockExecutor = redisLockExecutor;
|
||||||
|
this.workflowPluginDependencyService = workflowPluginDependencyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,15 +72,16 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
|
|||||||
@Override
|
@Override
|
||||||
public OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId) {
|
public OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId) {
|
||||||
List<OfflineImpactBindingVo> botBindings = listBotsByWorkflowId(workflowId);
|
List<OfflineImpactBindingVo> botBindings = listBotsByWorkflowId(workflowId);
|
||||||
|
List<OfflineImpactBindingVo> pluginBindings = workflowPluginDependencyService.listPluginsByWorkflowId(workflowId);
|
||||||
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
|
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
|
||||||
result.setCanProceed(true);
|
result.setCanProceed(true);
|
||||||
result.setBotBindings(botBindings);
|
result.setBotBindings(botBindings);
|
||||||
result.setHasBotBindings(!botBindings.isEmpty());
|
result.setHasBotBindings(!botBindings.isEmpty());
|
||||||
|
result.setPluginBindings(pluginBindings);
|
||||||
|
result.setHasPluginBindings(!pluginBindings.isEmpty());
|
||||||
result.setWorkflowUsages(Collections.emptyList());
|
result.setWorkflowUsages(Collections.emptyList());
|
||||||
result.setHasWorkflowUsages(false);
|
result.setHasWorkflowUsages(false);
|
||||||
result.setMessage(botBindings.isEmpty()
|
result.setMessage(resolveWorkflowOfflineImpactMessage(botBindings, pluginBindings));
|
||||||
? "当前工作流下线后不会影响已有绑定"
|
|
||||||
: "当前工作流下线成功后,将自动从相关聊天助手中解绑");
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +203,20 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
|
|||||||
return result;
|
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) {
|
private boolean containsKnowledgeReference(String content, BigInteger knowledgeId) {
|
||||||
if (!StringUtils.hasText(content) || knowledgeId == null) {
|
if (!StringUtils.hasText(content) || knowledgeId == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ public class OfflineImpactCheckVo {
|
|||||||
|
|
||||||
private boolean hasWorkflowUsages;
|
private boolean hasWorkflowUsages;
|
||||||
|
|
||||||
|
private boolean hasPluginBindings;
|
||||||
|
|
||||||
private List<OfflineImpactBindingVo> botBindings = new ArrayList<>();
|
private List<OfflineImpactBindingVo> botBindings = new ArrayList<>();
|
||||||
|
|
||||||
private List<OfflineImpactBindingVo> workflowUsages = new ArrayList<>();
|
private List<OfflineImpactBindingVo> workflowUsages = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<OfflineImpactBindingVo> pluginBindings = new ArrayList<>();
|
||||||
|
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,6 +114,22 @@ public class OfflineImpactCheckVo {
|
|||||||
this.workflowUsages = workflowUsages;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package tech.easyflow.system.service;
|
package tech.easyflow.system.service;
|
||||||
|
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -9,11 +10,17 @@ public interface CategoryPermissionService {
|
|||||||
|
|
||||||
boolean isCurrentSuperAdmin();
|
boolean isCurrentSuperAdmin();
|
||||||
|
|
||||||
|
boolean isSuperAdmin(LoginAccount loginAccount);
|
||||||
|
|
||||||
RoleCategoryAccessSnapshot getCurrentAccess(String resourceType);
|
RoleCategoryAccessSnapshot getCurrentAccess(String resourceType);
|
||||||
|
|
||||||
|
RoleCategoryAccessSnapshot getAccess(String resourceType, LoginAccount loginAccount);
|
||||||
|
|
||||||
Set<BigInteger> getCurrentVisibleCategoryIds(String resourceType);
|
Set<BigInteger> getCurrentVisibleCategoryIds(String resourceType);
|
||||||
|
|
||||||
boolean canAccessCategory(String resourceType, BigInteger createdBy, BigInteger categoryId);
|
boolean canAccessCategory(String resourceType, BigInteger createdBy, BigInteger categoryId);
|
||||||
|
|
||||||
|
boolean canAccessCategory(LoginAccount loginAccount, String resourceType, BigInteger createdBy, BigInteger categoryId);
|
||||||
|
|
||||||
void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message);
|
void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package tech.easyflow.system.service;
|
package tech.easyflow.system.service;
|
||||||
|
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.system.enums.CategoryResourceType;
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
import tech.easyflow.system.enums.ResourceAction;
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||||
@@ -8,5 +9,7 @@ public interface ResourceAccessService {
|
|||||||
|
|
||||||
boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action);
|
boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action);
|
||||||
|
|
||||||
|
boolean canAccess(LoginAccount loginAccount, CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action);
|
||||||
|
|
||||||
void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message);
|
void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ public class CategoryPermissionServiceImpl implements CategoryPermissionService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||||
|
return isSuperAdmin(loginAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSuperAdmin(LoginAccount loginAccount) {
|
||||||
return loginAccount != null && isSuperAdmin(loginAccount.getId());
|
return loginAccount != null && isSuperAdmin(loginAccount.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +58,11 @@ public class CategoryPermissionServiceImpl implements CategoryPermissionService
|
|||||||
return new RoleCategoryAccessSnapshot(resourceType, null, false, true, Collections.emptySet());
|
return new RoleCategoryAccessSnapshot(resourceType, null, false, true, Collections.emptySet());
|
||||||
}
|
}
|
||||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||||
|
return getAccess(resourceType, loginAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RoleCategoryAccessSnapshot getAccess(String resourceType, LoginAccount loginAccount) {
|
||||||
if (loginAccount == null) {
|
if (loginAccount == null) {
|
||||||
return new RoleCategoryAccessSnapshot(resourceType, null, false, true, Collections.emptySet());
|
return new RoleCategoryAccessSnapshot(resourceType, null, false, true, Collections.emptySet());
|
||||||
}
|
}
|
||||||
@@ -100,6 +110,12 @@ public class CategoryPermissionServiceImpl implements CategoryPermissionService
|
|||||||
return snapshot.canAccess(createdBy, categoryId);
|
return snapshot.canAccess(createdBy, categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canAccessCategory(LoginAccount loginAccount, String resourceType, BigInteger createdBy, BigInteger categoryId) {
|
||||||
|
RoleCategoryAccessSnapshot snapshot = getAccess(resourceType, loginAccount);
|
||||||
|
return snapshot.canAccess(createdBy, categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message) {
|
public void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message) {
|
||||||
if (!canAccessCategory(resourceType, createdBy, categoryId)) {
|
if (!canAccessCategory(resourceType, createdBy, categoryId)) {
|
||||||
|
|||||||
@@ -26,15 +26,19 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
|
public boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
|
||||||
|
return canAccess(SaTokenUtil.getLoginAccount(), resourceType, resource, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canAccess(LoginAccount loginAccount, CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
|
||||||
if (resource == null) {
|
if (resource == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
|
||||||
if (loginAccount == null || loginAccount.getId() == null) {
|
if (loginAccount == null || loginAccount.getId() == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
BigInteger accountId = loginAccount.getId();
|
BigInteger accountId = loginAccount.getId();
|
||||||
if (categoryPermissionService.isCurrentSuperAdmin()) {
|
if (categoryPermissionService.isSuperAdmin(loginAccount)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (accountId.equals(resource.getCreatedBy())) {
|
if (accountId.equals(resource.getCreatedBy())) {
|
||||||
@@ -43,7 +47,7 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
|
|||||||
if (ResourceAction.MANAGE == action) {
|
if (ResourceAction.MANAGE == action) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!categoryPermissionService.canAccessCategory(resourceType.getCode(), resource.getCreatedBy(), resource.getCategoryId())) {
|
if (!categoryPermissionService.canAccessCategory(loginAccount, resourceType.getCode(), resource.getCreatedBy(), resource.getCategoryId())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
VisibilityScope scope = VisibilityScope.fromOrDefault(resource.getVisibilityScope(), VisibilityScope.PRIVATE);
|
VisibilityScope scope = VisibilityScope.fromOrDefault(resource.getVisibilityScope(), VisibilityScope.PRIVATE);
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE tb_plugin_item
|
||||||
|
ADD COLUMN schema_hash varchar(128) NULL COMMENT '工作流插件输入输出契约哈希' AFTER english_name;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE `tb_plugin`
|
||||||
|
ADD COLUMN `workflow_id` bigint NULL DEFAULT NULL COMMENT '绑定工作流ID' AFTER `type`;
|
||||||
|
|
||||||
|
UPDATE `tb_plugin`
|
||||||
|
SET `type` = 1
|
||||||
|
WHERE `type` IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX `idx_tb_plugin_workflow_id` ON `tb_plugin` (`workflow_id`);
|
||||||
@@ -75,6 +75,9 @@
|
|||||||
"submitDeleteApprovalConfirm": "Delete the current workflow?",
|
"submitDeleteApprovalConfirm": "Delete the current workflow?",
|
||||||
"offlineImpactBoundBotsIntro": "This workflow is currently bound to the following bots:",
|
"offlineImpactBoundBotsIntro": "This workflow is currently bound to the following bots:",
|
||||||
"offlineImpactBoundBotsFooter": "After the workflow goes offline, the system will automatically remove it from these bots.",
|
"offlineImpactBoundBotsFooter": "After the workflow goes offline, the system will automatically remove it from these bots.",
|
||||||
|
"offlineImpactBoundPluginsIntro": "This workflow is currently bound to the following plugins:",
|
||||||
|
"offlineImpactBoundPluginsFooter": "After offline approval succeeds, these plugins will automatically become unavailable and show the reason in plugin management.",
|
||||||
|
"offlineImpactBoundMixedFooter": "After offline approval succeeds, the system will remove the workflow from bots and mark the related plugins as unavailable.",
|
||||||
"publishPendingHint": "There is already an approval in progress for this workflow.",
|
"publishPendingHint": "There is already an approval in progress for this workflow.",
|
||||||
"deletePendingHint": "There is already an approval in progress for this workflow.",
|
"deletePendingHint": "There is already an approval in progress for this workflow.",
|
||||||
"check": "Check",
|
"check": "Check",
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
"typeHttp": "HTTP Plugin",
|
||||||
|
"typeWorkflow": "Workflow Plugin",
|
||||||
"baseUrl": "BaseUrl",
|
"baseUrl": "BaseUrl",
|
||||||
|
"workflowId": "Bound workflow",
|
||||||
|
"workflowTitle": "Workflow title",
|
||||||
"authType": "AuthType",
|
"authType": "AuthType",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"icon": "Icon",
|
"icon": "Icon",
|
||||||
@@ -20,7 +24,8 @@
|
|||||||
"placeholder": {
|
"placeholder": {
|
||||||
"name": "Please enter plugin name",
|
"name": "Please enter plugin name",
|
||||||
"description": "Please enter plugin description",
|
"description": "Please enter plugin description",
|
||||||
"categorize": "Please enter categorize"
|
"categorize": "Please enter categorize",
|
||||||
|
"workflow": "Please select a published workflow"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"addPlugin": "Add Plugin",
|
"addPlugin": "Add Plugin",
|
||||||
@@ -29,5 +34,11 @@
|
|||||||
},
|
},
|
||||||
"toolsManagement": "Tools Management",
|
"toolsManagement": "Tools Management",
|
||||||
"searchUsers": "Search Users",
|
"searchUsers": "Search Users",
|
||||||
"parameterValue": "ParameterValue"
|
"parameterValue": "ParameterValue",
|
||||||
|
"workflow": "Workflow",
|
||||||
|
"workflowPluginHint": "Workflow plugins mirror the published snapshot of the target workflow. Availability is evaluated in real time against workflow permissions and approval status.",
|
||||||
|
"workflowPluginUnavailable": "This workflow plugin is unavailable",
|
||||||
|
"workflowSnapshotSynced": "Published snapshot synced",
|
||||||
|
"reasonMessage": "Reason",
|
||||||
|
"onlyPublishedWorkflow": "Only published workflows that you can currently access are selectable."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"debugStatus": "DebugStatus",
|
"debugStatus": "DebugStatus",
|
||||||
"englishName": "EnglishName",
|
"englishName": "EnglishName",
|
||||||
"createPluginTool": "Create tool",
|
"createPluginTool": "Create tool",
|
||||||
|
"systemManaged": "System synced",
|
||||||
"pluginToolEdit": {
|
"pluginToolEdit": {
|
||||||
"basicInfo": "Basic Info",
|
"basicInfo": "Basic Info",
|
||||||
"configureInputParameters": "Configure input parameters",
|
"configureInputParameters": "Configure input parameters",
|
||||||
@@ -21,11 +22,16 @@
|
|||||||
"toolPath": "Tool path",
|
"toolPath": "Tool path",
|
||||||
"requestMethod": "RequestMethod",
|
"requestMethod": "RequestMethod",
|
||||||
"runResult": "Run result",
|
"runResult": "Run result",
|
||||||
"run": "run"
|
"run": "run",
|
||||||
|
"workflowTarget": "Target workflow",
|
||||||
|
"unavailableHint": "The bound workflow is currently unavailable, so execution will not be started.",
|
||||||
|
"runWorkflowStepsEmpty": "After starting a trial run, each node execution result will be shown here.",
|
||||||
|
"workflowStepsPending": "The trial run has started. Waiting for node execution details..."
|
||||||
},
|
},
|
||||||
"parameterName": "Name",
|
"parameterName": "Name",
|
||||||
"parameterDescription": "Description",
|
"parameterDescription": "Description",
|
||||||
"parameterType": "Type",
|
"parameterType": "Type",
|
||||||
|
"direction": "Direction",
|
||||||
"inputMethod": "InputMethod",
|
"inputMethod": "InputMethod",
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
"defaultValue": "DefaultValue",
|
"defaultValue": "DefaultValue",
|
||||||
|
|||||||
@@ -75,6 +75,9 @@
|
|||||||
"submitDeleteApprovalConfirm": "确认删除当前工作流吗?",
|
"submitDeleteApprovalConfirm": "确认删除当前工作流吗?",
|
||||||
"offlineImpactBoundBotsIntro": "当前工作流被以下聊天助手绑定:",
|
"offlineImpactBoundBotsIntro": "当前工作流被以下聊天助手绑定:",
|
||||||
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该工作流。",
|
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该工作流。",
|
||||||
|
"offlineImpactBoundPluginsIntro": "当前工作流被以下插件绑定:",
|
||||||
|
"offlineImpactBoundPluginsFooter": "下线审批通过后,这些插件会自动变为不可用,并在插件页展示对应原因。",
|
||||||
|
"offlineImpactBoundMixedFooter": "下线审批通过后,系统会自动从聊天助手中解绑该工作流,同时让相关插件进入不可用状态。",
|
||||||
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||||
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||||
"check": "检查",
|
"check": "检查",
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
"name": "名称",
|
"name": "名称",
|
||||||
"description": "描述",
|
"description": "描述",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
|
"typeHttp": "HTTP 插件",
|
||||||
|
"typeWorkflow": "工作流插件",
|
||||||
"baseUrl": "基础URL",
|
"baseUrl": "基础URL",
|
||||||
|
"workflowId": "绑定工作流",
|
||||||
|
"workflowTitle": "工作流名称",
|
||||||
"authType": "认证方式",
|
"authType": "认证方式",
|
||||||
"created": "创建时间",
|
"created": "创建时间",
|
||||||
"icon": "图标地址",
|
"icon": "图标地址",
|
||||||
@@ -20,7 +24,8 @@
|
|||||||
"placeholder": {
|
"placeholder": {
|
||||||
"name": "请输入插件名称",
|
"name": "请输入插件名称",
|
||||||
"description": "请输入插件描述",
|
"description": "请输入插件描述",
|
||||||
"categorize": "请选择分类"
|
"categorize": "请选择分类",
|
||||||
|
"workflow": "请选择已发布工作流"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"addPlugin": "新增插件",
|
"addPlugin": "新增插件",
|
||||||
@@ -29,5 +34,11 @@
|
|||||||
},
|
},
|
||||||
"toolsManagement": "工具管理",
|
"toolsManagement": "工具管理",
|
||||||
"searchUsers": "搜索用户",
|
"searchUsers": "搜索用户",
|
||||||
"parameterValue": "参数值"
|
"parameterValue": "参数值",
|
||||||
|
"workflow": "工作流",
|
||||||
|
"workflowPluginHint": "工作流插件会自动镜像目标工作流的已发布快照,插件可用性会实时跟随工作流权限和审批状态变化。",
|
||||||
|
"workflowPluginUnavailable": "当前工作流插件不可用",
|
||||||
|
"workflowSnapshotSynced": "已同步发布快照",
|
||||||
|
"reasonMessage": "不可用原因",
|
||||||
|
"onlyPublishedWorkflow": "仅支持选择已发布且当前可访问的工作流。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"debugStatus": "调试状态【0失败 1成功】",
|
"debugStatus": "调试状态【0失败 1成功】",
|
||||||
"englishName": "英文名称",
|
"englishName": "英文名称",
|
||||||
"createPluginTool": "创建工具",
|
"createPluginTool": "创建工具",
|
||||||
|
"systemManaged": "系统同步",
|
||||||
"pluginToolEdit": {
|
"pluginToolEdit": {
|
||||||
"basicInfo": "基本信息",
|
"basicInfo": "基本信息",
|
||||||
"configureInputParameters": "配置输入参数",
|
"configureInputParameters": "配置输入参数",
|
||||||
@@ -21,11 +22,16 @@
|
|||||||
"toolPath": "工具路径",
|
"toolPath": "工具路径",
|
||||||
"requestMethod": "请求方法",
|
"requestMethod": "请求方法",
|
||||||
"runResult": "运行结果",
|
"runResult": "运行结果",
|
||||||
"run": "运行"
|
"run": "运行",
|
||||||
|
"workflowTarget": "目标工作流",
|
||||||
|
"unavailableHint": "当前绑定工作流不可用,本次不会发起执行。",
|
||||||
|
"runWorkflowStepsEmpty": "开始试运行后,这里会展示每个节点的执行结果。",
|
||||||
|
"workflowStepsPending": "试运行已发起,正在等待节点执行信息..."
|
||||||
},
|
},
|
||||||
"parameterName": "参数名称",
|
"parameterName": "参数名称",
|
||||||
"parameterDescription": "参数描述",
|
"parameterDescription": "参数描述",
|
||||||
"parameterType": "参数类型",
|
"parameterType": "参数类型",
|
||||||
|
"direction": "方向",
|
||||||
"inputMethod": "传入方法",
|
"inputMethod": "传入方法",
|
||||||
"required": "是否必填",
|
"required": "是否必填",
|
||||||
"defaultValue": "默认值",
|
"defaultValue": "默认值",
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance, FormRules } from 'element-plus';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import { Plus, Remove } from '@element-plus/icons-vue';
|
import { Plus, Remove } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
|
ElAlert,
|
||||||
|
ElButton,
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
@@ -22,15 +24,28 @@ import { api } from '#/api/request';
|
|||||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const emit = defineEmits(['reload']);
|
interface HeaderItem {
|
||||||
const embeddingLlmList = ref<any>([]);
|
|
||||||
const rerankerLlmList = ref<any>([]);
|
|
||||||
const categoryList = ref<any[]>([]);
|
|
||||||
interface headersType {
|
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
const authTypeList = ref<headersType[]>([
|
|
||||||
|
interface WorkflowCandidate {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(['reload']);
|
||||||
|
|
||||||
|
const saveForm = ref<FormInstance>();
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const isAdd = ref(true);
|
||||||
|
const btnLoading = ref(false);
|
||||||
|
const categoryList = ref<any[]>([]);
|
||||||
|
const workflowCandidates = ref<WorkflowCandidate[]>([]);
|
||||||
|
const tempAddHeaders = ref<HeaderItem[]>([]);
|
||||||
|
const entity = ref<any>(createDefaultEntity());
|
||||||
|
|
||||||
|
const authTypeList = [
|
||||||
{
|
{
|
||||||
label: 'None',
|
label: 'None',
|
||||||
value: 'none',
|
value: 'none',
|
||||||
@@ -39,79 +54,211 @@ const authTypeList = ref<headersType[]>([
|
|||||||
label: 'Service token / ApiKey',
|
label: 'Service token / ApiKey',
|
||||||
value: 'apiKey',
|
value: 'apiKey',
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
onMounted(() => {
|
|
||||||
api.get('/api/v1/plugin/modelList?supportEmbed=true').then((res) => {
|
const pluginTypeOptions = [
|
||||||
embeddingLlmList.value = res.data;
|
{
|
||||||
});
|
label: $t('plugin.typeHttp'),
|
||||||
api
|
value: 1,
|
||||||
.get('/api/v1/plugin/modelList?supportRerankerLlmList=true')
|
},
|
||||||
.then((res) => {
|
{
|
||||||
rerankerLlmList.value = res.data;
|
label: $t('plugin.typeWorkflow'),
|
||||||
});
|
value: 2,
|
||||||
api.get('/api/v1/pluginCategory/list').then((res) => {
|
},
|
||||||
if (res.errorCode === 0) {
|
];
|
||||||
categoryList.value = res.data;
|
|
||||||
}
|
const isWorkflowType = computed(
|
||||||
});
|
() => Number(entity.value.type || 1) === 2,
|
||||||
});
|
);
|
||||||
defineExpose({
|
|
||||||
openDialog,
|
const rules = computed<FormRules>(() => ({
|
||||||
});
|
|
||||||
const saveForm = ref<FormInstance>();
|
|
||||||
// variables
|
|
||||||
const dialogVisible = ref(false);
|
|
||||||
const isAdd = ref(true);
|
|
||||||
const tempAddHeaders = ref<headersType[]>([]);
|
|
||||||
const entity = ref<any>({
|
|
||||||
alias: '',
|
|
||||||
categoryIds: [],
|
|
||||||
deptId: '',
|
|
||||||
icon: '',
|
|
||||||
title: '',
|
|
||||||
authType: 'none',
|
|
||||||
description: '',
|
|
||||||
englishName: '',
|
|
||||||
headers: '',
|
|
||||||
position: '',
|
|
||||||
});
|
|
||||||
const btnLoading = ref(false);
|
|
||||||
const rules = ref({
|
|
||||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||||
description: [
|
description: [
|
||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||||
],
|
],
|
||||||
baseUrl: [
|
baseUrl: [
|
||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (!isWorkflowType.value && !value) {
|
||||||
|
callback(new Error($t('message.required')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workflowId: [
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (isWorkflowType.value && !value) {
|
||||||
|
callback(new Error($t('message.required')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'change',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
authType: [
|
authType: [
|
||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
{
|
||||||
],
|
validator: (_rule, value, callback) => {
|
||||||
tokenKey: [
|
if (!isWorkflowType.value && !value) {
|
||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
callback(new Error($t('message.required')));
|
||||||
],
|
return;
|
||||||
tokenValue: [
|
}
|
||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'change',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
position: [
|
position: [
|
||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (
|
||||||
|
!isWorkflowType.value &&
|
||||||
|
entity.value.authType === 'apiKey' &&
|
||||||
|
!value
|
||||||
|
) {
|
||||||
|
callback(new Error($t('message.required')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'change',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
tokenKey: [
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (
|
||||||
|
!isWorkflowType.value &&
|
||||||
|
entity.value.authType === 'apiKey' &&
|
||||||
|
!value
|
||||||
|
) {
|
||||||
|
callback(new Error($t('message.required')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tokenValue: [
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (
|
||||||
|
!isWorkflowType.value &&
|
||||||
|
entity.value.authType === 'apiKey' &&
|
||||||
|
!value
|
||||||
|
) {
|
||||||
|
callback(new Error($t('message.required')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => entity.value.type,
|
||||||
|
(type) => {
|
||||||
|
if (Number(type || 1) === 2) {
|
||||||
|
entity.value.baseUrl = '';
|
||||||
|
entity.value.authType = 'none';
|
||||||
|
entity.value.position = '';
|
||||||
|
entity.value.tokenKey = '';
|
||||||
|
entity.value.tokenValue = '';
|
||||||
|
tempAddHeaders.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.value.workflowId = '';
|
||||||
|
entity.value.workflowTitle = '';
|
||||||
|
entity.value.available = true;
|
||||||
|
entity.value.reasonMessage = '';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadCategories(), loadWorkflowCandidates()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// functions
|
defineExpose({
|
||||||
|
openDialog,
|
||||||
|
});
|
||||||
|
|
||||||
|
function createDefaultEntity() {
|
||||||
|
return {
|
||||||
|
alias: '',
|
||||||
|
authType: 'none',
|
||||||
|
available: true,
|
||||||
|
baseUrl: '',
|
||||||
|
categoryIds: [],
|
||||||
|
deptId: '',
|
||||||
|
description: '',
|
||||||
|
englishName: '',
|
||||||
|
headers: '',
|
||||||
|
icon: '',
|
||||||
|
name: '',
|
||||||
|
pluginType: 1,
|
||||||
|
position: '',
|
||||||
|
reasonMessage: '',
|
||||||
|
title: '',
|
||||||
|
tokenKey: '',
|
||||||
|
tokenValue: '',
|
||||||
|
type: 1,
|
||||||
|
workflowId: '',
|
||||||
|
workflowTitle: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCategories() {
|
||||||
|
const res = await api.get('/api/v1/pluginCategory/list');
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
categoryList.value = res.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkflowCandidates(keyword: string = '') {
|
||||||
|
const res = await api.get('/api/v1/plugin/workflowCandidates', {
|
||||||
|
params: {
|
||||||
|
keyword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
workflowCandidates.value = Array.isArray(res.data) ? res.data : [];
|
||||||
|
ensureCurrentWorkflowOption();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCurrentWorkflowOption() {
|
||||||
|
if (!entity.value.workflowId || !entity.value.workflowTitle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exists = workflowCandidates.value.some(
|
||||||
|
(item) => String(item.id) === String(entity.value.workflowId),
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
workflowCandidates.value.unshift({
|
||||||
|
id: entity.value.workflowId,
|
||||||
|
title: entity.value.workflowTitle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openDialog(row: any) {
|
function openDialog(row: any) {
|
||||||
tempAddHeaders.value = [];
|
tempAddHeaders.value = row.headers ? JSON.parse(row.headers) : [];
|
||||||
if (row.id) {
|
isAdd.value = !row.id;
|
||||||
isAdd.value = false;
|
|
||||||
if (row.headers) {
|
|
||||||
tempAddHeaders.value = JSON.parse(row.headers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entity.value = {
|
entity.value = {
|
||||||
|
...createDefaultEntity(),
|
||||||
...row,
|
...row,
|
||||||
authType: row.authType || 'none',
|
authType: row.authType || 'none',
|
||||||
categoryIds: row.categoryIds?.map((item: any) => item.id) || [],
|
categoryIds: row.categoryIds?.map((item: any) => item.id) || [],
|
||||||
|
type: Number(row.type || row.pluginType || 1),
|
||||||
};
|
};
|
||||||
|
ensureCurrentWorkflowOption();
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,83 +278,86 @@ async function syncPluginCategories(pluginId: string, categoryIds: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function normalizePayload() {
|
||||||
saveForm.value?.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
btnLoading.value = true;
|
|
||||||
const plainEntity = { ...entity.value };
|
const plainEntity = { ...entity.value };
|
||||||
const plainHeaders = [...tempAddHeaders.value];
|
|
||||||
const categoryIds = [...(plainEntity.categoryIds || [])];
|
const categoryIds = [...(plainEntity.categoryIds || [])];
|
||||||
delete plainEntity.categoryIds;
|
delete plainEntity.categoryIds;
|
||||||
if (isAdd.value) {
|
if (isWorkflowType.value) {
|
||||||
api
|
plainEntity.baseUrl = '';
|
||||||
.post('/api/v1/plugin/plugin/save', {
|
plainEntity.headers = [];
|
||||||
|
plainEntity.authType = 'none';
|
||||||
|
plainEntity.position = '';
|
||||||
|
plainEntity.tokenKey = '';
|
||||||
|
plainEntity.tokenValue = '';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
...plainEntity,
|
...plainEntity,
|
||||||
headers: plainHeaders,
|
headers: isWorkflowType.value ? [] : [...tempAddHeaders.value],
|
||||||
})
|
},
|
||||||
.then(async (res) => {
|
categoryIds,
|
||||||
if (res.errorCode === 0) {
|
};
|
||||||
const pluginId =
|
}
|
||||||
res.data?.id || plainEntity.id || entity.value.id;
|
|
||||||
|
function save() {
|
||||||
|
saveForm.value?.validate(async (valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btnLoading.value = true;
|
||||||
|
const { payload, categoryIds } = normalizePayload();
|
||||||
|
const requestUrl = isAdd.value
|
||||||
|
? '/api/v1/plugin/plugin/save'
|
||||||
|
: '/api/v1/plugin/plugin/update';
|
||||||
|
try {
|
||||||
|
const res = await api.post(requestUrl, payload);
|
||||||
|
if (res.errorCode !== 0) {
|
||||||
|
ElMessage.error(res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pluginId = res.data?.id || payload.id || entity.value.id;
|
||||||
if (!pluginId) {
|
if (!pluginId) {
|
||||||
throw new Error('插件保存成功,但未返回插件ID');
|
throw new Error('插件保存成功,但未返回插件ID');
|
||||||
}
|
}
|
||||||
await syncPluginCategories(pluginId, categoryIds);
|
await syncPluginCategories(pluginId, categoryIds);
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
ElMessage.success($t('message.saveOkMessage'));
|
ElMessage.success(
|
||||||
|
isAdd.value ? $t('message.saveOkMessage') : $t('message.updateOkMessage'),
|
||||||
|
);
|
||||||
emit('reload');
|
emit('reload');
|
||||||
} else {
|
} catch (error: any) {
|
||||||
ElMessage.error(res.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
||||||
})
|
} finally {
|
||||||
.finally(() => {
|
|
||||||
btnLoading.value = false;
|
btnLoading.value = false;
|
||||||
});
|
|
||||||
} else {
|
|
||||||
api
|
|
||||||
.post('/api/v1/plugin/plugin/update', {
|
|
||||||
...plainEntity,
|
|
||||||
headers: plainHeaders,
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
await syncPluginCategories(entity.value.id, categoryIds);
|
|
||||||
dialogVisible.value = false;
|
|
||||||
ElMessage.success($t('message.updateOkMessage'));
|
|
||||||
emit('reload');
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
btnLoading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
saveForm.value?.resetFields();
|
saveForm.value?.resetFields();
|
||||||
isAdd.value = true;
|
isAdd.value = true;
|
||||||
tempAddHeaders.value = [];
|
tempAddHeaders.value = [];
|
||||||
entity.value = {};
|
entity.value = createDefaultEntity();
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHeader() {
|
function addHeader() {
|
||||||
tempAddHeaders.value.push({
|
tempAddHeaders.value.push({
|
||||||
label: '',
|
label: '',
|
||||||
value: '',
|
value: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeHeader(index: number) {
|
function removeHeader(index: number) {
|
||||||
tempAddHeaders.value.splice(index, 1);
|
tempAddHeaders.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleWorkflowChange(value: string) {
|
||||||
|
const target = workflowCandidates.value.find(
|
||||||
|
(item) => String(item.id) === String(value),
|
||||||
|
);
|
||||||
|
entity.value.workflowTitle = target?.title || '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -237,15 +387,22 @@ function removeHeader(index: number) {
|
|||||||
>
|
>
|
||||||
<UploadAvatar v-model="entity.icon" />
|
<UploadAvatar v-model="entity.icon" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="type" :label="$t('plugin.type')">
|
||||||
|
<ElSelect v-model="entity.type" :disabled="!isAdd">
|
||||||
|
<ElOption
|
||||||
|
v-for="item in pluginTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
<ElFormItem prop="name" :label="$t('plugin.name')">
|
<ElFormItem prop="name" :label="$t('plugin.name')">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model.trim="entity.name"
|
v-model.trim="entity.name"
|
||||||
:placeholder="$t('plugin.placeholder.name')"
|
:placeholder="$t('plugin.placeholder.name')"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="baseUrl" :label="$t('plugin.baseUrl')">
|
|
||||||
<ElInput v-model.trim="entity.baseUrl" />
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem prop="description" :label="$t('plugin.description')">
|
<ElFormItem prop="description" :label="$t('plugin.description')">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model.trim="entity.description"
|
v-model.trim="entity.description"
|
||||||
@@ -270,21 +427,67 @@ function removeHeader(index: number) {
|
|||||||
/>
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<template v-if="isWorkflowType">
|
||||||
|
<ElAlert
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="mb-4"
|
||||||
|
:title="$t('plugin.workflowPluginHint')"
|
||||||
|
/>
|
||||||
|
<ElFormItem prop="workflowId" :label="$t('plugin.workflowId')">
|
||||||
|
<ElSelect
|
||||||
|
v-model="entity.workflowId"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
:remote-method="loadWorkflowCandidates"
|
||||||
|
:placeholder="$t('plugin.placeholder.workflow')"
|
||||||
|
@change="handleWorkflowChange"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="item in workflowCandidates"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.title"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
<div class="form-helper-text">
|
||||||
|
{{ $t('plugin.onlyPublishedWorkflow') }}
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElAlert
|
||||||
|
v-if="!entity.available && entity.reasonMessage"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
:title="$t('plugin.workflowPluginUnavailable')"
|
||||||
|
:description="entity.reasonMessage"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ElFormItem prop="baseUrl" :label="$t('plugin.baseUrl')">
|
||||||
|
<ElInput v-model.trim="entity.baseUrl" />
|
||||||
|
</ElFormItem>
|
||||||
<ElFormItem prop="Headers" label="Headers">
|
<ElFormItem prop="Headers" label="Headers">
|
||||||
<div
|
<div
|
||||||
class="headers-container-reduce flex flex-row gap-4"
|
|
||||||
v-for="(item, index) in tempAddHeaders"
|
v-for="(item, index) in tempAddHeaders"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
class="headers-container-reduce flex flex-row gap-4"
|
||||||
>
|
>
|
||||||
<div class="head-con-content flex flex-row gap-4">
|
<div class="head-con-content flex flex-row gap-4">
|
||||||
<ElInput v-model.trim="item.label" placeholder="header name" />
|
<ElInput v-model.trim="item.label" placeholder="header name" />
|
||||||
<ElInput v-model.trim="item.value" placeholder="header value" />
|
<ElInput v-model.trim="item.value" placeholder="header value" />
|
||||||
<ElIcon size="20" @click="removeHeader" style="cursor: pointer">
|
<ElIcon
|
||||||
|
size="20"
|
||||||
|
style="cursor: pointer"
|
||||||
|
@click="removeHeader(index)"
|
||||||
|
>
|
||||||
<Remove />
|
<Remove />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ElButton @click="addHeader" class="addHeadersBtn">
|
<ElButton class="addHeadersBtn" @click="addHeader">
|
||||||
<ElIcon size="18" style="margin-right: 4px">
|
<ElIcon size="18" style="margin-right: 4px">
|
||||||
<Plus />
|
<Plus />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
@@ -302,9 +505,9 @@ function removeHeader(index: number) {
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
|
v-if="entity.authType === 'apiKey'"
|
||||||
prop="position"
|
prop="position"
|
||||||
:label="$t('plugin.position')"
|
:label="$t('plugin.position')"
|
||||||
v-if="entity.authType === 'apiKey'"
|
|
||||||
>
|
>
|
||||||
<ElRadioGroup v-model="entity.position">
|
<ElRadioGroup v-model="entity.position">
|
||||||
<ElRadio value="headers">headers</ElRadio>
|
<ElRadio value="headers">headers</ElRadio>
|
||||||
@@ -312,19 +515,20 @@ function removeHeader(index: number) {
|
|||||||
</ElRadioGroup>
|
</ElRadioGroup>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
|
v-if="entity.authType === 'apiKey'"
|
||||||
prop="tokenKey"
|
prop="tokenKey"
|
||||||
:label="$t('plugin.tokenKey')"
|
:label="$t('plugin.tokenKey')"
|
||||||
v-if="entity.authType === 'apiKey'"
|
|
||||||
>
|
>
|
||||||
<ElInput v-model.trim="entity.tokenKey" />
|
<ElInput v-model.trim="entity.tokenKey" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
|
v-if="entity.authType === 'apiKey'"
|
||||||
prop="tokenValue"
|
prop="tokenValue"
|
||||||
:label="$t('plugin.tokenValue')"
|
:label="$t('plugin.tokenValue')"
|
||||||
v-if="entity.authType === 'apiKey'"
|
|
||||||
>
|
>
|
||||||
<ElInput v-model.trim="entity.tokenValue" />
|
<ElInput v-model.trim="entity.tokenValue" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</EasyFlowFormModal>
|
</EasyFlowFormModal>
|
||||||
</template>
|
</template>
|
||||||
@@ -346,4 +550,11 @@ function removeHeader(index: number) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-helper-text {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
ElInput,
|
ElInput,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
ElMessageBox,
|
ElMessageBox,
|
||||||
|
ElTag,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
@@ -94,6 +95,10 @@ const actions: ActionButton[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const pluginTypeTagMap = {
|
||||||
|
1: $t('plugin.typeHttp'),
|
||||||
|
2: $t('plugin.typeWorkflow'),
|
||||||
|
};
|
||||||
const categoryList = ref<PluginCategory[]>([]);
|
const categoryList = ref<PluginCategory[]>([]);
|
||||||
const controlBtns = [
|
const controlBtns = [
|
||||||
{
|
{
|
||||||
@@ -265,11 +270,26 @@ const handleClickCategory = (item: PluginCategory) => {
|
|||||||
title-field="title"
|
title-field="title"
|
||||||
icon-field="icon"
|
icon-field="icon"
|
||||||
desc-field="description"
|
desc-field="description"
|
||||||
|
tag-field="type"
|
||||||
|
:tag-map="pluginTypeTagMap"
|
||||||
:data="pageList"
|
:data="pageList"
|
||||||
:primary-action="primaryAction"
|
:primary-action="primaryAction"
|
||||||
:actions="actions"
|
:actions="actions"
|
||||||
:default-icon="defaultPluginIcon"
|
:default-icon="defaultPluginIcon"
|
||||||
/>
|
>
|
||||||
|
<template #corner="{ item }">
|
||||||
|
<ElTag
|
||||||
|
v-if="item.type === 2 && item.available === false"
|
||||||
|
type="danger"
|
||||||
|
effect="light"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
item.reasonMessage || $t('plugin.workflowPluginUnavailable')
|
||||||
|
}}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</CardPage>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
modelValue: () => [],
|
modelValue: () => [],
|
||||||
editable: false,
|
editable: false,
|
||||||
isEditOutput: false,
|
isEditOutput: false,
|
||||||
|
payloadMode: 'plugin',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
export interface TreeTableNode {
|
export interface TreeTableNode {
|
||||||
key: string;
|
key: string;
|
||||||
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
method?: 'Body' | 'Header' | 'Path' | 'Query';
|
method?: 'Body' | 'Header' | 'Path' | 'Query';
|
||||||
@@ -29,6 +31,7 @@ interface Props {
|
|||||||
modelValue?: TreeTableNode[];
|
modelValue?: TreeTableNode[];
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
isEditOutput?: boolean;
|
isEditOutput?: boolean;
|
||||||
|
payloadMode?: 'plugin' | 'workflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = ref<TreeTableNode[]>([]);
|
const data = ref<TreeTableNode[]>([]);
|
||||||
@@ -48,8 +51,12 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 计算缩进宽度
|
// 计算缩进宽度
|
||||||
|
const getNodeKey = (record?: Partial<TreeTableNode>): string => {
|
||||||
|
return String(record?.key || record?.id || '');
|
||||||
|
};
|
||||||
|
|
||||||
const getIndentWidth = (record: TreeTableNode): number => {
|
const getIndentWidth = (record: TreeTableNode): number => {
|
||||||
const level = String(record.key).split('-').length - 1;
|
const level = getNodeKey(record).split('-').length - 1;
|
||||||
const indentSize = 20;
|
const indentSize = 20;
|
||||||
return level > 0 ? level * indentSize : 0;
|
return level > 0 ? level * indentSize : 0;
|
||||||
};
|
};
|
||||||
@@ -65,7 +72,7 @@ const onExpand = (_row: TreeTableNode, expandedRows: TreeTableNode[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 验证字段
|
// 验证字段
|
||||||
const validateFields = (): boolean => {
|
const validatePluginFields = (): boolean => {
|
||||||
const newErrors: Record<
|
const newErrors: Record<
|
||||||
string,
|
string,
|
||||||
Partial<Record<keyof TreeTableNode, string>>
|
Partial<Record<keyof TreeTableNode, string>>
|
||||||
@@ -75,6 +82,7 @@ const validateFields = (): boolean => {
|
|||||||
const checkNode = (node: TreeTableNode): boolean => {
|
const checkNode = (node: TreeTableNode): boolean => {
|
||||||
const { name, description, method, type } = node;
|
const { name, description, method, type } = node;
|
||||||
const nodeErrors: Partial<Record<keyof TreeTableNode, string>> = {};
|
const nodeErrors: Partial<Record<keyof TreeTableNode, string>> = {};
|
||||||
|
const nodeKey = getNodeKey(node);
|
||||||
|
|
||||||
if (!name?.trim()) {
|
if (!name?.trim()) {
|
||||||
nodeErrors.name = $t('message.cannotBeEmpty.name');
|
nodeErrors.name = $t('message.cannotBeEmpty.name');
|
||||||
@@ -96,8 +104,8 @@ const validateFields = (): boolean => {
|
|||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(nodeErrors).length > 0) {
|
if (nodeKey && Object.keys(nodeErrors).length > 0) {
|
||||||
newErrors[node.key] = nodeErrors;
|
newErrors[nodeKey] = nodeErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
@@ -117,17 +125,118 @@ const validateFields = (): boolean => {
|
|||||||
return isValid;
|
return isValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBlankValue = (value: unknown): boolean => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim().length === 0;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.length === 0;
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return Object.keys(value as Record<string, unknown>).length === 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeNodeType = (node: TreeTableNode): string => {
|
||||||
|
return String(node.type || '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseWorkflowNodeValue = (node: TreeTableNode): any => {
|
||||||
|
const type = normalizeNodeType(node);
|
||||||
|
if (node.children?.length) {
|
||||||
|
if (type === 'Object') {
|
||||||
|
return buildWorkflowPayload(node.children);
|
||||||
|
}
|
||||||
|
if (type.includes('Array')) {
|
||||||
|
return parseArrayValue(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type.includes('Array')) {
|
||||||
|
return parseArrayValue(node);
|
||||||
|
}
|
||||||
|
return node.defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseArrayValue = (node: TreeTableNode): any[] => {
|
||||||
|
if (Array.isArray(node.defaultValue as any)) {
|
||||||
|
return node.defaultValue as any[];
|
||||||
|
}
|
||||||
|
const raw = String(node.defaultValue || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return Array.isArray(parsed) ? parsed : [parsed];
|
||||||
|
} catch {
|
||||||
|
return [raw];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildWorkflowPayload = (nodes: TreeTableNode[]): Record<string, any> => {
|
||||||
|
const payload: Record<string, any> = {};
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (!node?.name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
payload[node.name] = parseWorkflowNodeValue(node);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateWorkflowFields = (): boolean => {
|
||||||
|
const newErrors: Record<
|
||||||
|
string,
|
||||||
|
Partial<Record<keyof TreeTableNode, string>>
|
||||||
|
> = {};
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
const checkNode = (node: TreeTableNode) => {
|
||||||
|
const nodeKey = getNodeKey(node);
|
||||||
|
const nodeErrors: Partial<Record<keyof TreeTableNode, string>> = {};
|
||||||
|
const value = parseWorkflowNodeValue(node);
|
||||||
|
|
||||||
|
if (node.required && isBlankValue(value)) {
|
||||||
|
nodeErrors.defaultValue = $t('message.required');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeKey && Object.keys(nodeErrors).length > 0) {
|
||||||
|
newErrors[nodeKey] = nodeErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.children?.forEach(checkNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
data.value.forEach((node) => {
|
||||||
|
checkNode(node);
|
||||||
|
});
|
||||||
|
errors.value = newErrors;
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
// 判断是否为根节点
|
// 判断是否为根节点
|
||||||
const isRootNode = (record: TreeTableNode): boolean => {
|
const isRootNode = (record: TreeTableNode): boolean => {
|
||||||
return !record.key.includes('-');
|
return !getNodeKey(record).includes('-');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交参数
|
// 提交参数
|
||||||
const handleSubmitParams = () => {
|
const handleSubmitParams = () => {
|
||||||
if (!validateFields()) {
|
const valid =
|
||||||
|
props.payloadMode === 'workflow'
|
||||||
|
? validateWorkflowFields()
|
||||||
|
: validatePluginFields();
|
||||||
|
if (valid !== true) {
|
||||||
ElMessage.error($t('message.completeForm'));
|
ElMessage.error($t('message.completeForm'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (props.payloadMode === 'workflow') {
|
||||||
|
return buildWorkflowPayload(data.value);
|
||||||
|
}
|
||||||
return data.value;
|
return data.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,7 +254,7 @@ interface Emits {
|
|||||||
<div class="tree-table-container">
|
<div class="tree-table-container">
|
||||||
<ElTable
|
<ElTable
|
||||||
:data="data"
|
:data="data"
|
||||||
row-key="key"
|
:row-key="getNodeKey"
|
||||||
:border="true"
|
:border="true"
|
||||||
size="default"
|
size="default"
|
||||||
:expand-row-keys="expandedKeys"
|
:expand-row-keys="expandedKeys"
|
||||||
@@ -160,7 +269,7 @@ interface Emits {
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="name-cell">
|
<div class="name-cell">
|
||||||
<div
|
<div
|
||||||
v-if="!props.editable"
|
v-if="!props.editable || props.payloadMode === 'workflow'"
|
||||||
:style="{ paddingLeft: `${getIndentWidth(row)}px` }"
|
:style="{ paddingLeft: `${getIndentWidth(row)}px` }"
|
||||||
>
|
>
|
||||||
{{ row.name || '' }}
|
{{ row.name || '' }}
|
||||||
@@ -175,11 +284,11 @@ interface Emits {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="errors[row.key]?.name"
|
v-if="errors[getNodeKey(row)]?.name"
|
||||||
class="error-message"
|
class="error-message"
|
||||||
:style="{ marginLeft: `${getIndentWidth(row)}px` }"
|
:style="{ marginLeft: `${getIndentWidth(row)}px` }"
|
||||||
>
|
>
|
||||||
{{ errors[row.key]?.name }}
|
{{ errors[getNodeKey(row)]?.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,12 +304,19 @@ interface Emits {
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.type === 'Object'"></span>
|
<span v-if="row.type === 'Object'"></span>
|
||||||
<span v-else-if="!props.editable">{{ row.defaultValue || '' }}</span>
|
<span v-else-if="!props.editable">{{ row.defaultValue || '' }}</span>
|
||||||
|
<div v-else class="value-input-wrapper">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-else
|
|
||||||
v-model="row.defaultValue"
|
v-model="row.defaultValue"
|
||||||
@input="handleDataChange"
|
@input="handleDataChange"
|
||||||
:disabled="!props.editable"
|
:disabled="!props.editable"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="errors[getNodeKey(row)]?.defaultValue"
|
||||||
|
class="error-message"
|
||||||
|
>
|
||||||
|
{{ errors[getNodeKey(row)]?.defaultValue }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</ElTable>
|
</ElTable>
|
||||||
@@ -243,6 +359,12 @@ interface Emits {
|
|||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.action-buttons .el-button {
|
.action-buttons .el-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { nextTick, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
import { preferences } from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
|
import { sortNodes } from '@easyflow/utils';
|
||||||
|
|
||||||
import { VideoPlay } from '@element-plus/icons-vue';
|
import { VideoPlay } from '@element-plus/icons-vue';
|
||||||
import { ElButton, ElMenu, ElMenuItem } from 'element-plus';
|
import { ElAlert, ElButton, ElMenu, ElMenuItem, ElMessage } from 'element-plus';
|
||||||
import { JsonViewer } from 'vue3-json-viewer';
|
import { JsonViewer } from 'vue3-json-viewer';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import PluginRunParams from '#/views/ai/plugin/PluginRunParams.vue';
|
import PluginRunParams from '#/views/ai/plugin/PluginRunParams.vue';
|
||||||
|
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
|
||||||
|
|
||||||
import 'vue3-json-viewer/dist/vue3-json-viewer.css';
|
import 'vue3-json-viewer/dist/vue3-json-viewer.css';
|
||||||
|
|
||||||
@@ -26,42 +28,159 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const openDialog = () => {
|
|
||||||
getPluginToolInfo();
|
|
||||||
runResultResponse.value = null;
|
|
||||||
dialogVisible.value = true;
|
|
||||||
};
|
|
||||||
const runTitle = ref('');
|
const runTitle = ref('');
|
||||||
const runResult = ref('');
|
const runResult = ref('');
|
||||||
const inputDataParams = ref<any>(null);
|
const inputDataParams = ref<any>(null);
|
||||||
const runResultResponse = ref<any>(null);
|
const runResultResponse = ref<any>(null);
|
||||||
function getPluginToolInfo() {
|
const runParamsRef = ref();
|
||||||
api
|
const runLoading = ref(false);
|
||||||
.post('/api/v1/pluginItem/tool/search', {
|
const pluginAvailable = ref(true);
|
||||||
|
const pluginReasonMessage = ref('');
|
||||||
|
const isWorkflowPlugin = ref(false);
|
||||||
|
const workflowId = ref<null | string>(null);
|
||||||
|
const workflowSnapshot = ref<any>(null);
|
||||||
|
const workflowNodeJson = ref<any[]>([]);
|
||||||
|
const pollingNodes = ref<any[]>([]);
|
||||||
|
const executeId = ref('');
|
||||||
|
const pollingData = ref<any>({ nodes: {} });
|
||||||
|
const initSignal = ref(false);
|
||||||
|
const pollingTimer = ref<null | ReturnType<typeof setInterval>>(null);
|
||||||
|
const activeIndex = ref('1');
|
||||||
|
const dialogContentKey = ref(0);
|
||||||
|
const dialogPreparing = ref(false);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openDialog,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openDialog() {
|
||||||
|
if (dialogPreparing.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialogPreparing.value = true;
|
||||||
|
resetDialogState();
|
||||||
|
const ready = await getPluginToolInfo();
|
||||||
|
dialogPreparing.value = false;
|
||||||
|
if (!ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialogContentKey.value += 1;
|
||||||
|
await nextTick();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetExecutionState() {
|
||||||
|
stopPolling();
|
||||||
|
runResultResponse.value = null;
|
||||||
|
runLoading.value = false;
|
||||||
|
executeId.value = '';
|
||||||
|
pollingData.value = { nodes: {} };
|
||||||
|
initSignal.value = !initSignal.value;
|
||||||
|
activeIndex.value = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDialogState() {
|
||||||
|
resetExecutionState();
|
||||||
|
runTitle.value = '';
|
||||||
|
runResult.value = '';
|
||||||
|
inputDataParams.value = [];
|
||||||
|
pluginAvailable.value = true;
|
||||||
|
pluginReasonMessage.value = '';
|
||||||
|
isWorkflowPlugin.value = false;
|
||||||
|
workflowId.value = null;
|
||||||
|
workflowSnapshot.value = null;
|
||||||
|
workflowNodeJson.value = [];
|
||||||
|
pollingNodes.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPluginToolInfo() {
|
||||||
|
try {
|
||||||
|
const res = await api.post('/api/v1/pluginItem/tool/search', {
|
||||||
aiPluginToolId: props.pluginToolId,
|
aiPluginToolId: props.pluginToolId,
|
||||||
})
|
});
|
||||||
.then((res) => {
|
if (res.errorCode !== 0 || !res.data) {
|
||||||
if (res.errorCode === 0) {
|
ElMessage.error(res?.message || '加载试运行信息失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
runTitle.value = `${res.data.aiPlugin.title} - ${res.data.data.name} ${$t(
|
runTitle.value = `${res.data.aiPlugin.title} - ${res.data.data.name} ${$t(
|
||||||
'pluginItem.inputData',
|
'pluginItem.inputData',
|
||||||
)}`;
|
)}`;
|
||||||
runResult.value = `${$t('pluginItem.pluginToolEdit.runResult')}`;
|
runResult.value = `${$t('pluginItem.pluginToolEdit.runResult')}`;
|
||||||
inputDataParams.value = JSON.parse(res.data.data.inputData || '[]');
|
inputDataParams.value = JSON.parse(res.data.data.inputData || '[]');
|
||||||
|
pluginAvailable.value = res.data.aiPlugin?.available !== false;
|
||||||
|
pluginReasonMessage.value = res.data.aiPlugin?.reasonMessage || '';
|
||||||
|
isWorkflowPlugin.value = Number(res.data.aiPlugin?.type || 1) === 2;
|
||||||
|
workflowId.value = res.data.aiPlugin?.workflowId
|
||||||
|
? String(res.data.aiPlugin.workflowId)
|
||||||
|
: null;
|
||||||
|
workflowSnapshot.value = res.data.workflowSnapshot || null;
|
||||||
|
hydrateWorkflowNodes(workflowSnapshot.value);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(buildErrorResult(error).error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const activeIndex = ref('1');
|
|
||||||
defineExpose({
|
function hydrateWorkflowNodes(snapshot: any) {
|
||||||
openDialog,
|
workflowNodeJson.value = [];
|
||||||
});
|
pollingNodes.value = [];
|
||||||
function handleSelect(index: string) {
|
const content = snapshot?.content;
|
||||||
activeIndex.value = index;
|
if (!content) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const runParamsRef = ref();
|
try {
|
||||||
const runLoading = ref(false);
|
const workflowContent = JSON.parse(content);
|
||||||
|
workflowNodeJson.value = sortNodes(workflowContent) || [];
|
||||||
|
pollingNodes.value = Array.isArray(workflowContent?.nodes)
|
||||||
|
? workflowContent.nodes.map((node: any) => ({
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeName: node?.data?.title || node.id,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析工作流插件快照失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildErrorResult(error: any) {
|
||||||
|
const responseData = error?.response?.data ?? {};
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
responseData?.error ||
|
||||||
|
responseData?.message ||
|
||||||
|
error?.message ||
|
||||||
|
'试运行失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUnavailableResult() {
|
||||||
|
runResultResponse.value = {
|
||||||
|
skipped: true,
|
||||||
|
reasonMessage:
|
||||||
|
pluginReasonMessage.value ||
|
||||||
|
$t('pluginItem.pluginToolEdit.unavailableHint'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubmitRun() {
|
function handleSubmitRun() {
|
||||||
|
if (!pluginAvailable.value) {
|
||||||
|
buildUnavailableResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const runParams = runParamsRef.value?.handleSubmitParams?.();
|
||||||
|
if (runParams === null || runParams === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isWorkflowPlugin.value) {
|
||||||
|
handleWorkflowSubmit(runParams);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleHttpSubmit(runParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHttpSubmit(runParams: any) {
|
||||||
runLoading.value = true;
|
runLoading.value = true;
|
||||||
const runParams = runParamsRef.value.handleSubmitParams();
|
|
||||||
api
|
api
|
||||||
.post('/api/v1/pluginItem/test', {
|
.post('/api/v1/pluginItem/test', {
|
||||||
pluginToolId: props.pluginToolId,
|
pluginToolId: props.pluginToolId,
|
||||||
@@ -72,9 +191,160 @@ function handleSubmitRun() {
|
|||||||
runResultResponse.value = res.data;
|
runResultResponse.value = res.data;
|
||||||
activeIndex.value = '2';
|
activeIndex.value = '2';
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
runResultResponse.value = buildErrorResult(error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
runLoading.value = false;
|
runLoading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleWorkflowSubmit(runParams: any) {
|
||||||
|
if (!workflowId.value) {
|
||||||
|
runResultResponse.value = {
|
||||||
|
error: '当前插件未绑定有效工作流,无法试运行。',
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetExecutionState();
|
||||||
|
runLoading.value = true;
|
||||||
|
api
|
||||||
|
.post('/api/v1/pluginItem/testAsync', {
|
||||||
|
pluginToolId: props.pluginToolId,
|
||||||
|
inputData: JSON.stringify(runParams),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.errorCode === 0 && res.data) {
|
||||||
|
executeId.value = String(res.data);
|
||||||
|
pollingData.value = {
|
||||||
|
executeId: executeId.value,
|
||||||
|
status: 1,
|
||||||
|
nodes: {},
|
||||||
|
};
|
||||||
|
runResultResponse.value = pollingData.value;
|
||||||
|
activeIndex.value = '2';
|
||||||
|
startPolling(executeId.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
runResultResponse.value = buildErrorResult(error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
runLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling(nextExecuteId: string) {
|
||||||
|
stopPolling();
|
||||||
|
pollingTimer.value = setInterval(() => {
|
||||||
|
executePolling(nextExecuteId);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearInterval(pollingTimer.value);
|
||||||
|
pollingTimer.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executePolling(nextExecuteId: string) {
|
||||||
|
api
|
||||||
|
.post('/api/v1/pluginItem/testChainStatus', {
|
||||||
|
executeId: nextExecuteId,
|
||||||
|
nodes: pollingNodes.value,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.errorCode !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextData = {
|
||||||
|
...res.data,
|
||||||
|
nodes: res.data?.nodes || {},
|
||||||
|
};
|
||||||
|
pollingData.value = nextData;
|
||||||
|
runResultResponse.value = nextData;
|
||||||
|
if (nextData.status !== 1) {
|
||||||
|
stopPolling();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
stopPolling();
|
||||||
|
runResultResponse.value = buildErrorResult(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeChain(payload: any) {
|
||||||
|
if (!executeId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api
|
||||||
|
.post('/api/v1/pluginItem/testResume', {
|
||||||
|
executeId: executeId.value,
|
||||||
|
confirmParams: payload?.confirmParams || {},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
startPolling(executeId.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
runResultResponse.value = buildErrorResult(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWorkflowSteps() {
|
||||||
|
return isWorkflowPlugin.value === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWorkflowStepList() {
|
||||||
|
return showWorkflowSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWorkflowResultAlert() {
|
||||||
|
return (
|
||||||
|
showWorkflowSteps() &&
|
||||||
|
(!!runResultResponse.value?.error || !!runResultResponse.value?.skipped)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function workflowResultAlertType() {
|
||||||
|
return runResultResponse.value?.skipped ? 'warning' : 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
function workflowResultAlertMessage() {
|
||||||
|
return (
|
||||||
|
runResultResponse.value?.reasonMessage ||
|
||||||
|
runResultResponse.value?.error ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWorkflowStepsEmpty() {
|
||||||
|
return showWorkflowSteps() && workflowNodeJson.value.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHttpResultEmpty() {
|
||||||
|
return (
|
||||||
|
showWorkflowSteps() !== true &&
|
||||||
|
activeIndex.value === '2' &&
|
||||||
|
!runResultResponse.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(index: string) {
|
||||||
|
activeIndex.value = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
stopPolling();
|
||||||
|
dialogVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -83,27 +353,37 @@ function handleSubmitRun() {
|
|||||||
width="80%"
|
width="80%"
|
||||||
align-center
|
align-center
|
||||||
:title="$t('pluginItem.pluginToolEdit.trialRun')"
|
:title="$t('pluginItem.pluginToolEdit.trialRun')"
|
||||||
:before-close="() => (dialogVisible = false)"
|
:before-close="closeDialog"
|
||||||
>
|
>
|
||||||
<div class="run-test-container">
|
<div :key="dialogContentKey" class="run-test-container">
|
||||||
<div class="run-test-params">
|
<div class="run-test-params">
|
||||||
<div class="run-title-style">
|
<div class="run-title-style">
|
||||||
{{ runTitle }}
|
{{ runTitle }}
|
||||||
</div>
|
</div>
|
||||||
|
<ElAlert
|
||||||
|
v-if="!pluginAvailable"
|
||||||
|
class="mb-4"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
:title="$t('pluginItem.pluginToolEdit.unavailableHint')"
|
||||||
|
:description="pluginReasonMessage"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<PluginRunParams
|
<PluginRunParams
|
||||||
v-model="inputDataParams"
|
|
||||||
:editable="true"
|
|
||||||
:is-edit-output="true"
|
|
||||||
ref="runParamsRef"
|
ref="runParamsRef"
|
||||||
|
v-model="inputDataParams"
|
||||||
|
:editable="pluginAvailable"
|
||||||
|
:is-edit-output="true"
|
||||||
|
:payload-mode="isWorkflowPlugin ? 'workflow' : 'plugin'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="run-test-result">
|
<div class="run-test-result">
|
||||||
<div class="run-title-style">
|
<div class="run-title-style">
|
||||||
{{ runResult }}
|
{{ showWorkflowSteps() ? $t('aiWorkflow.steps') : runResult }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="!showWorkflowSteps()">
|
||||||
<ElMenu
|
<ElMenu
|
||||||
:default-active="activeIndex"
|
:default-active="activeIndex"
|
||||||
class="el-menu-demo"
|
class="el-menu-demo"
|
||||||
@@ -116,6 +396,28 @@ function handleSubmitRun() {
|
|||||||
</ElMenu>
|
</ElMenu>
|
||||||
</div>
|
</div>
|
||||||
<div class="run-res-json">
|
<div class="run-res-json">
|
||||||
|
<template v-if="showWorkflowSteps()">
|
||||||
|
<ElAlert
|
||||||
|
v-if="showWorkflowResultAlert()"
|
||||||
|
class="run-result-alert"
|
||||||
|
:type="workflowResultAlertType()"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
:title="workflowResultAlertMessage()"
|
||||||
|
/>
|
||||||
|
<WorkflowSteps
|
||||||
|
v-if="showWorkflowStepList()"
|
||||||
|
:workflow-id="workflowId"
|
||||||
|
:node-json="workflowNodeJson"
|
||||||
|
:init-signal="initSignal"
|
||||||
|
:polling-data="pollingData"
|
||||||
|
@resume="resumeChain"
|
||||||
|
/>
|
||||||
|
<div v-if="showWorkflowStepsEmpty()" class="run-result-placeholder">
|
||||||
|
{{ $t('pluginItem.pluginToolEdit.runWorkflowStepsEmpty') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<JsonViewer
|
<JsonViewer
|
||||||
v-if="activeIndex === '1'"
|
v-if="activeIndex === '1'"
|
||||||
:value="inputDataParams || {}"
|
:value="inputDataParams || {}"
|
||||||
@@ -124,24 +426,28 @@ function handleSubmitRun() {
|
|||||||
:theme="themeMode"
|
:theme="themeMode"
|
||||||
/>
|
/>
|
||||||
<JsonViewer
|
<JsonViewer
|
||||||
v-if="activeIndex === '2'"
|
v-if="activeIndex === '2' && runResultResponse"
|
||||||
:value="runResultResponse || {}"
|
:value="runResultResponse || {}"
|
||||||
copyable
|
copyable
|
||||||
:expand-depth="Infinity"
|
:expand-depth="Infinity"
|
||||||
:theme="themeMode"
|
:theme="themeMode"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="showHttpResultEmpty()" class="run-result-placeholder">
|
||||||
|
{{ $t('common.noDataAvailable') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ElButton @click="dialogVisible = false">
|
<ElButton @click="closeDialog">
|
||||||
{{ $t('button.cancel') }}
|
{{ $t('button.cancel') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
type="primary"
|
type="primary"
|
||||||
:icon="VideoPlay"
|
:icon="VideoPlay"
|
||||||
@click="handleSubmitRun"
|
|
||||||
:loading="runLoading"
|
:loading="runLoading"
|
||||||
|
@click="handleSubmitRun"
|
||||||
>
|
>
|
||||||
{{ $t('pluginItem.pluginToolEdit.run') }}
|
{{ $t('pluginItem.pluginToolEdit.run') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -163,21 +469,34 @@ function handleSubmitRun() {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-res-json {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.run-test-result {
|
.run-test-result {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-cell {
|
.run-res-json {
|
||||||
position: relative;
|
flex: 1;
|
||||||
min-width: 100%;
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-result-alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-result-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 240px;
|
||||||
|
padding: 24px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--el-fill-color-extra-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-title-style {
|
.run-title-style {
|
||||||
@@ -186,30 +505,6 @@ function handleSubmitRun() {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editable-name {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-input-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-input-wrapper .el-input {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
margin-top: 2px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.2;
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table td.el-table__cell.first-column div) {
|
:deep(.el-table td.el-table__cell.first-column div) {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
@@ -14,23 +14,19 @@ import {
|
|||||||
ElMessage,
|
ElMessage,
|
||||||
ElOption,
|
ElOption,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
|
ElTable,
|
||||||
|
ElTableColumn,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import PluginInputAndOutParams from '#/views/ai/plugin/PluginInputAndOutParams.vue';
|
import PluginInputAndOutParams from '#/views/ai/plugin/PluginInputAndOutParams.vue';
|
||||||
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
||||||
|
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const toolId = ref<string>((route.query.id as string) || '');
|
const toolId = ref<string>((route.query.id as string) || '');
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!toolId.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getPluginToolInfo();
|
|
||||||
});
|
|
||||||
const pluginToolInfo = ref<any>({
|
const pluginToolInfo = ref<any>({
|
||||||
name: '',
|
name: '',
|
||||||
englishName: '',
|
englishName: '',
|
||||||
@@ -41,23 +37,12 @@ const pluginToolInfo = ref<any>({
|
|||||||
const pluginInfo = ref<any>({});
|
const pluginInfo = ref<any>({});
|
||||||
const pluginInputData = ref<any[]>([]);
|
const pluginInputData = ref<any[]>([]);
|
||||||
const pluginOutputData = ref<any[]>([]);
|
const pluginOutputData = ref<any[]>([]);
|
||||||
|
|
||||||
function getPluginToolInfo() {
|
|
||||||
api
|
|
||||||
.post('/api/v1/pluginItem/tool/search', {
|
|
||||||
aiPluginToolId: toolId.value,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
pluginToolInfo.value = res.data.data;
|
|
||||||
pluginInfo.value = res.data.aiPlugin;
|
|
||||||
pluginInputData.value = JSON.parse(res.data.data.inputData || '[]');
|
|
||||||
pluginOutputData.value = JSON.parse(res.data.data.outputData || '[]');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const pluginInputParamsEditable = ref(false);
|
const pluginInputParamsEditable = ref(false);
|
||||||
const pluginOutputParamsEditable = ref(false);
|
const pluginOutputParamsEditable = ref(false);
|
||||||
|
const isWorkflowPlugin = ref(false);
|
||||||
|
const pluginUnavailable = ref(false);
|
||||||
|
const pluginUnavailableReason = ref('');
|
||||||
|
const workflowSnapshot = ref<any>(null);
|
||||||
|
|
||||||
const pluginBasicCollapse = ref({
|
const pluginBasicCollapse = ref({
|
||||||
title: $t('pluginItem.pluginToolEdit.basicInfo'),
|
title: $t('pluginItem.pluginToolEdit.basicInfo'),
|
||||||
@@ -74,33 +59,12 @@ const pluginBasicCollapseOutputParams = ref({
|
|||||||
isOpen: false,
|
isOpen: false,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pluginInputParamsRef = ref();
|
const pluginInputParamsRef = ref();
|
||||||
const pluginOutputParamsRef = ref();
|
const pluginOutputParamsRef = ref();
|
||||||
const handleClickHeader = (index: number) => {
|
const saveForm = ref();
|
||||||
switch (index) {
|
const runTestRef = ref();
|
||||||
case 1: {
|
|
||||||
pluginBasicCollapse.value.isOpen = !pluginBasicCollapse.value.isOpen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
pluginBasicCollapseInputParams.value.isOpen =
|
|
||||||
!pluginBasicCollapseInputParams.value.isOpen;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
pluginBasicCollapseOutputParams.value.isOpen =
|
|
||||||
!pluginBasicCollapseOutputParams.value.isOpen;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const back = () => {
|
|
||||||
router.back();
|
|
||||||
};
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||||
requestMethod: [
|
requestMethod: [
|
||||||
@@ -125,118 +89,7 @@ const rules = reactive({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const saveForm = ref();
|
|
||||||
const updatePluginTool = (index: number) => {
|
|
||||||
if (index === 1) {
|
|
||||||
if (!saveForm.value) return;
|
|
||||||
saveForm.value.validate((valid: boolean) => {
|
|
||||||
if (valid) {
|
|
||||||
updatePluginToolInfo(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
updatePluginToolInfo(index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const updatePluginToolInfo = (index: number) => {
|
|
||||||
api
|
|
||||||
.post('/api/v1/pluginItem/tool/update', {
|
|
||||||
id: toolId.value,
|
|
||||||
name: pluginToolInfo.value.name,
|
|
||||||
englishName: pluginToolInfo.value.englishName,
|
|
||||||
description: pluginToolInfo.value.description,
|
|
||||||
basePath: pluginToolInfo.value.basePath,
|
|
||||||
requestMethod: pluginToolInfo.value.requestMethod,
|
|
||||||
inputData: JSON.stringify(pluginInputData.value),
|
|
||||||
outputData: JSON.stringify(pluginOutputData.value),
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
ElMessage.success($t('message.updateOkMessage'));
|
|
||||||
switch (index) {
|
|
||||||
case 1: {
|
|
||||||
pluginBasicCollapse.value.isEdit = false;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
pluginBasicCollapseInputParams.value.isEdit = false;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
pluginBasicCollapseOutputParams.value.isEdit = false;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleEdit = (index: number) => {
|
|
||||||
switch (index) {
|
|
||||||
case 1: {
|
|
||||||
pluginBasicCollapse.value.isEdit = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
pluginBasicCollapseInputParams.value.isEdit = true;
|
|
||||||
pluginBasicCollapseInputParams.value.isOpen = true;
|
|
||||||
pluginInputParamsEditable.value = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
pluginBasicCollapseOutputParams.value.isEdit = true;
|
|
||||||
pluginBasicCollapseOutputParams.value.isOpen = true;
|
|
||||||
pluginOutputParamsEditable.value = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleSave = (index: number) => {
|
|
||||||
if (index === 2) {
|
|
||||||
try {
|
|
||||||
// 调用校验方法,若抛异常则进入 catch
|
|
||||||
pluginInputParamsRef.value.handleSubmitParams();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('校验失败:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (index === 3) {
|
|
||||||
try {
|
|
||||||
pluginOutputParamsRef.value.handleSubmitParams();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('校验失败:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pluginInputParamsEditable.value = false;
|
|
||||||
updatePluginTool(index);
|
|
||||||
};
|
|
||||||
const handleCancel = (index: number) => {
|
|
||||||
getPluginToolInfo();
|
|
||||||
switch (index) {
|
|
||||||
case 1: {
|
|
||||||
pluginBasicCollapse.value.isEdit = false;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
pluginBasicCollapseInputParams.value.isEdit = false;
|
|
||||||
pluginInputParamsEditable.value = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
pluginBasicCollapseOutputParams.value.isEdit = false;
|
|
||||||
pluginOutputParamsEditable.value = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const requestMethodOptions = [
|
const requestMethodOptions = [
|
||||||
{
|
{
|
||||||
label: 'POST',
|
label: 'POST',
|
||||||
@@ -259,30 +112,259 @@ const requestMethodOptions = [
|
|||||||
value: 'PATCH',
|
value: 'PATCH',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const runTestRef = ref();
|
|
||||||
const handleOpenRunModal = () => {
|
onMounted(() => {
|
||||||
|
if (!toolId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getPluginToolInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowParameterRows = computed(() => [
|
||||||
|
...flattenParameterRows(pluginInputData.value, 'input'),
|
||||||
|
...flattenParameterRows(pluginOutputData.value, 'output'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getPluginToolInfo() {
|
||||||
|
api
|
||||||
|
.post('/api/v1/pluginItem/tool/search', {
|
||||||
|
aiPluginToolId: toolId.value,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
pluginToolInfo.value = res.data.data;
|
||||||
|
pluginInfo.value = res.data.aiPlugin;
|
||||||
|
pluginInputData.value = JSON.parse(res.data.data.inputData || '[]');
|
||||||
|
pluginOutputData.value = JSON.parse(res.data.data.outputData || '[]');
|
||||||
|
isWorkflowPlugin.value = Number(res.data.aiPlugin?.type || 1) === 2;
|
||||||
|
pluginUnavailable.value = res.data.aiPlugin?.available === false;
|
||||||
|
pluginUnavailableReason.value = res.data.aiPlugin?.reasonMessage || '';
|
||||||
|
workflowSnapshot.value = res.data.workflowSnapshot || null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickHeader(index: number) {
|
||||||
|
switch (index) {
|
||||||
|
case 1: {
|
||||||
|
pluginBasicCollapse.value.isOpen = !pluginBasicCollapse.value.isOpen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
pluginBasicCollapseInputParams.value.isOpen =
|
||||||
|
!pluginBasicCollapseInputParams.value.isOpen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
pluginBasicCollapseOutputParams.value.isOpen =
|
||||||
|
!pluginBasicCollapseOutputParams.value.isOpen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function back() {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePluginTool(index: number) {
|
||||||
|
if (index === 1) {
|
||||||
|
if (!saveForm.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveForm.value.validate((valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
updatePluginToolInfo(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updatePluginToolInfo(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePluginToolInfo(index: number) {
|
||||||
|
api
|
||||||
|
.post('/api/v1/pluginItem/tool/update', {
|
||||||
|
id: toolId.value,
|
||||||
|
name: pluginToolInfo.value.name,
|
||||||
|
englishName: pluginToolInfo.value.englishName,
|
||||||
|
description: pluginToolInfo.value.description,
|
||||||
|
basePath: pluginToolInfo.value.basePath,
|
||||||
|
requestMethod: pluginToolInfo.value.requestMethod,
|
||||||
|
inputData: JSON.stringify(pluginInputData.value),
|
||||||
|
outputData: JSON.stringify(pluginOutputData.value),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
ElMessage.success($t('message.updateOkMessage'));
|
||||||
|
switch (index) {
|
||||||
|
case 1: {
|
||||||
|
pluginBasicCollapse.value.isEdit = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
pluginBasicCollapseInputParams.value.isEdit = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
pluginBasicCollapseOutputParams.value.isEdit = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(index: number) {
|
||||||
|
if (isWorkflowPlugin.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (index) {
|
||||||
|
case 1: {
|
||||||
|
pluginBasicCollapse.value.isEdit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
pluginBasicCollapseInputParams.value.isEdit = true;
|
||||||
|
pluginBasicCollapseInputParams.value.isOpen = true;
|
||||||
|
pluginInputParamsEditable.value = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
pluginBasicCollapseOutputParams.value.isEdit = true;
|
||||||
|
pluginBasicCollapseOutputParams.value.isOpen = true;
|
||||||
|
pluginOutputParamsEditable.value = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave(index: number) {
|
||||||
|
if (index === 2) {
|
||||||
|
pluginInputParamsRef.value.handleSubmitParams();
|
||||||
|
}
|
||||||
|
if (index === 3) {
|
||||||
|
pluginOutputParamsRef.value.handleSubmitParams();
|
||||||
|
}
|
||||||
|
pluginInputParamsEditable.value = false;
|
||||||
|
updatePluginTool(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel(index: number) {
|
||||||
|
getPluginToolInfo();
|
||||||
|
switch (index) {
|
||||||
|
case 1: {
|
||||||
|
pluginBasicCollapse.value.isEdit = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
pluginBasicCollapseInputParams.value.isEdit = false;
|
||||||
|
pluginInputParamsEditable.value = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
pluginBasicCollapseOutputParams.value.isEdit = false;
|
||||||
|
pluginOutputParamsEditable.value = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenRunModal() {
|
||||||
runTestRef.value.openDialog();
|
runTestRef.value.openDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenParameterRows(
|
||||||
|
parameters: any[] = [],
|
||||||
|
direction: 'input' | 'output',
|
||||||
|
parentPath = '',
|
||||||
|
): any[] {
|
||||||
|
return parameters.flatMap((item) => {
|
||||||
|
const currentName = String(item?.name || '').trim();
|
||||||
|
const currentPath = currentName
|
||||||
|
? parentPath
|
||||||
|
? `${parentPath}.${currentName}`
|
||||||
|
: currentName
|
||||||
|
: parentPath || '-';
|
||||||
|
const currentRow = {
|
||||||
|
key: String(item?.id || item?.key || currentPath),
|
||||||
|
direction,
|
||||||
|
name: currentPath,
|
||||||
|
type: String(item?.dataType || item?.type || '-'),
|
||||||
};
|
};
|
||||||
|
const children = Array.isArray(item?.children)
|
||||||
|
? flattenParameterRows(item.children, direction, currentPath)
|
||||||
|
: [];
|
||||||
|
return [currentRow, ...children];
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="accordion-container">
|
<div class="accordion-container">
|
||||||
<div class="controls-header">
|
<div class="controls-header">
|
||||||
<ElButton @click="back" :icon="Back">
|
<ElButton :icon="Back" @click="back">
|
||||||
{{ $t('button.back') }}
|
{{ $t('button.back') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton type="primary" :icon="VideoPlay" @click="handleOpenRunModal">
|
<ElButton type="primary" :icon="VideoPlay" @click="handleOpenRunModal">
|
||||||
{{ $t('pluginItem.pluginToolEdit.trialRun') }}
|
{{ $t('pluginItem.pluginToolEdit.trialRun') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<!-- 折叠面板列表 -->
|
<template v-if="isWorkflowPlugin">
|
||||||
<div class="accordion-list">
|
<div class="workflow-tool-layout">
|
||||||
<!-- 基本信息-->
|
<section class="workflow-tool-card">
|
||||||
|
<div class="workflow-tool-card__header">
|
||||||
|
<div class="workflow-tool-card__title">
|
||||||
|
{{ $t('pluginItem.inputData') }} / {{ $t('pluginItem.outputData') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElTable
|
||||||
|
:data="workflowParameterRows"
|
||||||
|
border
|
||||||
|
class="workflow-tool-table"
|
||||||
|
empty-text="-"
|
||||||
|
>
|
||||||
|
<ElTableColumn
|
||||||
|
prop="direction"
|
||||||
|
:label="$t('pluginItem.direction')"
|
||||||
|
width="120"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.direction === 'input' ? $t('pluginItem.inputData') : $t('pluginItem.outputData') }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn
|
||||||
|
prop="name"
|
||||||
|
:label="$t('pluginItem.parameterName')"
|
||||||
|
min-width="280"
|
||||||
|
/>
|
||||||
|
<ElTableColumn
|
||||||
|
prop="type"
|
||||||
|
:label="$t('pluginItem.parameterType')"
|
||||||
|
min-width="160"
|
||||||
|
/>
|
||||||
|
</ElTable>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="workflow-tool-card">
|
||||||
|
<div class="workflow-tool-card__header">
|
||||||
|
<div class="workflow-tool-card__title">
|
||||||
|
快照
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<WorkflowApprovalSnapshotPreview
|
||||||
|
:title="workflowSnapshot?.title || pluginInfo.workflowTitle || pluginToolInfo.name"
|
||||||
|
:description="''"
|
||||||
|
:content="workflowSnapshot?.content || ''"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="accordion-list">
|
||||||
<div
|
<div
|
||||||
class="accordion-item"
|
class="accordion-item"
|
||||||
:class="{ 'accordion-item--active': pluginBasicCollapse.isOpen }"
|
:class="{ 'accordion-item--active': pluginBasicCollapse.isOpen }"
|
||||||
>
|
>
|
||||||
<!-- 面板头部 -->
|
|
||||||
<div class="accordion-header" @click="handleClickHeader(1)">
|
<div class="accordion-header" @click="handleClickHeader(1)">
|
||||||
<div class="column-header-container">
|
<div class="column-header-container">
|
||||||
<div
|
<div
|
||||||
@@ -295,37 +377,38 @@ const handleOpenRunModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="accordion-title">{{ pluginBasicCollapse.title }}</h3>
|
<h3 class="accordion-title">{{ pluginBasicCollapse.title }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="!isWorkflowPlugin">
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleEdit(1)"
|
|
||||||
type="primary"
|
|
||||||
v-if="!pluginBasicCollapse.isEdit"
|
v-if="!pluginBasicCollapse.isEdit"
|
||||||
|
type="primary"
|
||||||
|
@click.stop="handleEdit(1)"
|
||||||
>
|
>
|
||||||
{{ $t('button.edit') }}
|
{{ $t('button.edit') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleCancel(1)"
|
|
||||||
v-if="pluginBasicCollapse.isEdit"
|
v-if="pluginBasicCollapse.isEdit"
|
||||||
|
@click.stop="handleCancel(1)"
|
||||||
>
|
>
|
||||||
{{ $t('button.cancel') }}
|
{{ $t('button.cancel') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleSave(1)"
|
|
||||||
type="primary"
|
|
||||||
v-if="pluginBasicCollapse.isEdit"
|
v-if="pluginBasicCollapse.isEdit"
|
||||||
|
type="primary"
|
||||||
|
@click.stop="handleSave(1)"
|
||||||
>
|
>
|
||||||
{{ $t('button.save') }}
|
{{ $t('button.save') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
<ElTag v-else type="info" effect="plain">
|
||||||
|
{{ $t('pluginItem.systemManaged') }}
|
||||||
|
</ElTag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 面板内容 -->
|
|
||||||
<div
|
<div
|
||||||
class="accordion-content"
|
class="accordion-content"
|
||||||
:class="{ 'accordion-content--open': pluginBasicCollapse.isOpen }"
|
:class="{ 'accordion-content--open': pluginBasicCollapse.isOpen }"
|
||||||
>
|
>
|
||||||
<div class="accordion-content-inner">
|
<div class="accordion-content-inner">
|
||||||
<!--编辑基本信息-->
|
|
||||||
<div v-show="pluginBasicCollapse.isEdit">
|
<div v-show="pluginBasicCollapse.isEdit">
|
||||||
<div class="plugin-tool-info-edit-container">
|
<div class="plugin-tool-info-edit-container">
|
||||||
<ElForm
|
<ElForm
|
||||||
@@ -381,7 +464,6 @@ const handleOpenRunModal = () => {
|
|||||||
</ElForm>
|
</ElForm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--显示基本信息-->
|
|
||||||
<div
|
<div
|
||||||
v-show="!pluginBasicCollapse.isEdit"
|
v-show="!pluginBasicCollapse.isEdit"
|
||||||
class="plugin-tool-info-view-container"
|
class="plugin-tool-info-view-container"
|
||||||
@@ -402,6 +484,13 @@ const handleOpenRunModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>{{ pluginToolInfo.description }}</div>
|
<div>{{ pluginToolInfo.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isWorkflowPlugin" class="plugin-tool-view-item">
|
||||||
|
<div class="view-item-title">
|
||||||
|
{{ $t('pluginItem.pluginToolEdit.workflowTarget') }}:
|
||||||
|
</div>
|
||||||
|
<div>{{ pluginInfo.workflowTitle || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
<div class="plugin-tool-view-item">
|
<div class="plugin-tool-view-item">
|
||||||
<div class="view-item-title">
|
<div class="view-item-title">
|
||||||
{{ $t('pluginItem.pluginToolEdit.toolPath') }}:
|
{{ $t('pluginItem.pluginToolEdit.toolPath') }}:
|
||||||
@@ -416,18 +505,18 @@ const handleOpenRunModal = () => {
|
|||||||
{{ pluginToolInfo.requestMethod }}
|
{{ pluginToolInfo.requestMethod }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 输入参数-->
|
|
||||||
<div
|
<div
|
||||||
class="accordion-item"
|
class="accordion-item"
|
||||||
:class="{
|
:class="{
|
||||||
'accordion-item--active': pluginBasicCollapseInputParams.isOpen,
|
'accordion-item--active': pluginBasicCollapseInputParams.isOpen,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- 面板头部 -->
|
|
||||||
<div class="accordion-header" @click="handleClickHeader(2)">
|
<div class="accordion-header" @click="handleClickHeader(2)">
|
||||||
<div class="column-header-container">
|
<div class="column-header-container">
|
||||||
<div
|
<div
|
||||||
@@ -445,31 +534,30 @@ const handleOpenRunModal = () => {
|
|||||||
{{ pluginBasicCollapseInputParams.title }}
|
{{ pluginBasicCollapseInputParams.title }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="!isWorkflowPlugin">
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleEdit(2)"
|
|
||||||
type="primary"
|
|
||||||
v-if="!pluginBasicCollapseInputParams.isEdit"
|
v-if="!pluginBasicCollapseInputParams.isEdit"
|
||||||
|
type="primary"
|
||||||
|
@click.stop="handleEdit(2)"
|
||||||
>
|
>
|
||||||
{{ $t('button.edit') }}
|
{{ $t('button.edit') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleCancel(2)"
|
|
||||||
v-if="pluginBasicCollapseInputParams.isEdit"
|
v-if="pluginBasicCollapseInputParams.isEdit"
|
||||||
|
@click.stop="handleCancel(2)"
|
||||||
>
|
>
|
||||||
{{ $t('button.cancel') }}
|
{{ $t('button.cancel') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleSave(2)"
|
|
||||||
type="primary"
|
|
||||||
v-if="pluginBasicCollapseInputParams.isEdit"
|
v-if="pluginBasicCollapseInputParams.isEdit"
|
||||||
|
type="primary"
|
||||||
|
@click.stop="handleSave(2)"
|
||||||
>
|
>
|
||||||
{{ $t('button.save') }}
|
{{ $t('button.save') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--输入参数-->
|
|
||||||
<div
|
<div
|
||||||
class="accordion-content"
|
class="accordion-content"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -486,14 +574,13 @@ const handleOpenRunModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 输出参数-->
|
|
||||||
<div
|
<div
|
||||||
class="accordion-item"
|
class="accordion-item"
|
||||||
:class="{
|
:class="{
|
||||||
'accordion-item--active': pluginBasicCollapseOutputParams.isOpen,
|
'accordion-item--active': pluginBasicCollapseOutputParams.isOpen,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- 面板头部 -->
|
|
||||||
<div class="accordion-header" @click="handleClickHeader(3)">
|
<div class="accordion-header" @click="handleClickHeader(3)">
|
||||||
<div class="column-header-container">
|
<div class="column-header-container">
|
||||||
<div
|
<div
|
||||||
@@ -511,31 +598,30 @@ const handleOpenRunModal = () => {
|
|||||||
{{ pluginBasicCollapseOutputParams.title }}
|
{{ pluginBasicCollapseOutputParams.title }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="!isWorkflowPlugin">
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleEdit(3)"
|
|
||||||
type="primary"
|
|
||||||
v-if="!pluginBasicCollapseOutputParams.isEdit"
|
v-if="!pluginBasicCollapseOutputParams.isEdit"
|
||||||
|
type="primary"
|
||||||
|
@click.stop="handleEdit(3)"
|
||||||
>
|
>
|
||||||
{{ $t('button.edit') }}
|
{{ $t('button.edit') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleCancel(3)"
|
|
||||||
v-if="pluginBasicCollapseOutputParams.isEdit"
|
v-if="pluginBasicCollapseOutputParams.isEdit"
|
||||||
|
@click.stop="handleCancel(3)"
|
||||||
>
|
>
|
||||||
{{ $t('button.cancel') }}
|
{{ $t('button.cancel') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
@click.stop="handleSave(3)"
|
|
||||||
type="primary"
|
|
||||||
v-if="pluginBasicCollapseOutputParams.isEdit"
|
v-if="pluginBasicCollapseOutputParams.isEdit"
|
||||||
|
type="primary"
|
||||||
|
@click.stop="handleSave(3)"
|
||||||
>
|
>
|
||||||
{{ $t('button.save') }}
|
{{ $t('button.save') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--输出参数-->
|
|
||||||
<div
|
<div
|
||||||
class="accordion-content"
|
class="accordion-content"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -544,8 +630,8 @@ const handleOpenRunModal = () => {
|
|||||||
>
|
>
|
||||||
<div class="accordion-content-inner">
|
<div class="accordion-content-inner">
|
||||||
<PluginInputAndOutParams
|
<PluginInputAndOutParams
|
||||||
v-model="pluginOutputData"
|
|
||||||
ref="pluginOutputParamsRef"
|
ref="pluginOutputParamsRef"
|
||||||
|
v-model="pluginOutputData"
|
||||||
:editable="pluginOutputParamsEditable"
|
:editable="pluginOutputParamsEditable"
|
||||||
:is-edit-output="true"
|
:is-edit-output="true"
|
||||||
/>
|
/>
|
||||||
@@ -553,211 +639,133 @@ const handleOpenRunModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 试运行模态框-->
|
|
||||||
<PluginRunTestModal ref="runTestRef" :plugin-tool-id="toolId" />
|
<PluginRunTestModal ref="runTestRef" :plugin-tool-id="toolId" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.accordion-container {
|
.accordion-container {
|
||||||
padding: 15px;
|
display: flex;
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 16px;
|
||||||
align-items: stretch;
|
padding: 24px;
|
||||||
}
|
|
||||||
|
|
||||||
.control-group {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordion-header {
|
|
||||||
padding: 14px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordion-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordion-container {
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 0 auto;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-header {
|
.controls-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制面板样式 */
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn:hover {
|
|
||||||
background: #3498db;
|
|
||||||
background: var(--el-color-primary-light-9);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠面板列表 */
|
|
||||||
.accordion-list {
|
.accordion-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
padding-top: 20px;
|
}
|
||||||
|
|
||||||
|
.workflow-tool-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-tool-card {
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-tool-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-tool-card__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-tool-table {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-item {
|
.accordion-item {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: hsl(var(--background));
|
background: var(--el-bg-color);
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid var(--el-border-color-light);
|
||||||
border-radius: 8px;
|
border-radius: 16px;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-item:hover {
|
.accordion-item--active {
|
||||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
border-color: var(--el-color-primary-light-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-header {
|
.accordion-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px 20px;
|
padding: 18px 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
|
||||||
background: hsl(var(--background));
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-title {
|
.column-header-container {
|
||||||
padding-left: 12px;
|
display: flex;
|
||||||
margin: 0;
|
gap: 10px;
|
||||||
font-size: 1.1rem;
|
align-items: center;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-icon {
|
.accordion-icon {
|
||||||
font-size: 12px;
|
display: inline-flex;
|
||||||
color: #7f8c8d;
|
transition: transform 0.2s ease;
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-icon--rotated {
|
.accordion-icon--rotated {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accordion-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.accordion-content {
|
.accordion-content {
|
||||||
max-height: 0;
|
display: grid;
|
||||||
overflow: hidden;
|
grid-template-rows: 0fr;
|
||||||
background: hsl(var(--background));
|
transition: grid-template-rows 0.2s ease;
|
||||||
transition: max-height 0.4s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-content--open {
|
.accordion-content--open {
|
||||||
max-height: 2000px;
|
grid-template-rows: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-content-inner {
|
.accordion-content-inner {
|
||||||
padding: 20px;
|
min-height: 0;
|
||||||
border-top: 1px solid hsl(var(--border));
|
padding: 0 20px 20px;
|
||||||
}
|
overflow: hidden;
|
||||||
|
|
||||||
.accordion-content-inner p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-header-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-tool-info-view-container {
|
.plugin-tool-info-view-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 25px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-tool-view-item {
|
.plugin-tool-view-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
line-height: 22px;
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-item-title {
|
.view-item-title {
|
||||||
width: 70px;
|
min-width: 108px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
/* text-align: right; */
|
|
||||||
|
|
||||||
/* margin-right: 12px; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
pluginType: {
|
||||||
|
default: 1,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -100,7 +104,7 @@ const pluginToolReload = () => {
|
|||||||
{{ $t('button.edit') }}
|
{{ $t('button.edit') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|
||||||
<ElDropdown>
|
<ElDropdown v-if="Number(props.pluginType || 1) !== 2">
|
||||||
<ElButton link :icon="MoreFilled" />
|
<ElButton link :icon="MoreFilled" />
|
||||||
|
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { markRaw, ref } from 'vue';
|
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import { Back, Plus } from '@element-plus/icons-vue';
|
import { Back, Plus } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
import { api } from '#/api/request';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
import PluginToolTable from '#/views/ai/plugin/PluginToolTable.vue';
|
import PluginToolTable from '#/views/ai/plugin/PluginToolTable.vue';
|
||||||
|
|
||||||
@@ -13,26 +14,49 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const pluginId = ref<string>((route.query.id as string) || '');
|
const pluginId = ref<string>((route.query.id as string) || '');
|
||||||
const headerButtons = [
|
const pluginInfo = ref<any>({});
|
||||||
|
const pluginToolRef = ref();
|
||||||
|
|
||||||
|
const headerButtons = computed<any[]>(() => {
|
||||||
|
const buttons: any[] = [
|
||||||
{
|
{
|
||||||
key: 'back',
|
key: 'back',
|
||||||
text: $t('button.back'),
|
text: $t('button.back'),
|
||||||
icon: markRaw(Back),
|
icon: markRaw(Back),
|
||||||
data: { action: 'back' },
|
data: { action: 'back' },
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
if (Number(pluginInfo.value?.type || 1) !== 2) {
|
||||||
|
buttons.push({
|
||||||
key: 'createTool',
|
key: 'createTool',
|
||||||
text: $t('pluginItem.createPluginTool'),
|
text: $t('pluginItem.createPluginTool'),
|
||||||
icon: markRaw(Plus),
|
icon: markRaw(Plus),
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
data: { action: 'createTool' },
|
data: { action: 'createTool' },
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
const handleSearch = (params: any) => {
|
return buttons;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadPluginInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadPluginInfo() {
|
||||||
|
if (!pluginId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await api.get(`/api/v1/plugin/detail?id=${pluginId.value}`);
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
pluginInfo.value = res.data || {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch(params: any) {
|
||||||
pluginToolRef.value.handleSearch(params);
|
pluginToolRef.value.handleSearch(params);
|
||||||
};
|
}
|
||||||
const handleButtonClick = (event: any) => {
|
|
||||||
// 根据按钮 key 执行不同操作
|
function handleButtonClick(event: any) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'back': {
|
case 'back': {
|
||||||
router.push({ path: '/ai/plugin' });
|
router.push({ path: '/ai/plugin' });
|
||||||
@@ -43,8 +67,7 @@ const handleButtonClick = (event: any) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const pluginToolRef = ref();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -56,7 +79,11 @@ const pluginToolRef = ref();
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
||||||
<PluginToolTable :plugin-id="pluginId" ref="pluginToolRef" />
|
<PluginToolTable
|
||||||
|
ref="pluginToolRef"
|
||||||
|
:plugin-id="pluginId"
|
||||||
|
:plugin-type="pluginInfo.type"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ export interface OfflineImpactBinding {
|
|||||||
export interface OfflineImpactCheck {
|
export interface OfflineImpactCheck {
|
||||||
canProceed: boolean;
|
canProceed: boolean;
|
||||||
hasBotBindings: boolean;
|
hasBotBindings: boolean;
|
||||||
|
hasPluginBindings: boolean;
|
||||||
hasWorkflowUsages: boolean;
|
hasWorkflowUsages: boolean;
|
||||||
botBindings: OfflineImpactBinding[];
|
botBindings: OfflineImpactBinding[];
|
||||||
|
pluginBindings: OfflineImpactBinding[];
|
||||||
workflowUsages: OfflineImpactBinding[];
|
workflowUsages: OfflineImpactBinding[];
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -605,9 +605,25 @@ function handlePluginNodeUpdate(chooseId: any) {
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
pageLoading.value = false;
|
pageLoading.value = false;
|
||||||
updatePluginNode.value(res.data);
|
updatePluginNode.value(decoratePluginNodeData(res.data));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function decoratePluginNodeData(data: any) {
|
||||||
|
if (!data || Number(data.pluginType || 1) !== 2) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const statusText =
|
||||||
|
data.available === false
|
||||||
|
? data.reasonMessage || $t('plugin.workflowPluginUnavailable')
|
||||||
|
: $t('plugin.workflowSnapshotSynced');
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
pluginName: data.pluginName
|
||||||
|
? `${data.pluginName} · ${statusText}`
|
||||||
|
: statusText,
|
||||||
|
pluginStatusText: statusText,
|
||||||
|
};
|
||||||
|
}
|
||||||
function onAsyncExecute(info: any) {
|
function onAsyncExecute(info: any) {
|
||||||
chainInfo.value = info;
|
chainInfo.value = info;
|
||||||
}
|
}
|
||||||
@@ -628,7 +644,7 @@ function onAsyncExecute(info: any) {
|
|||||||
:title="$t('menus.ai.plugin')"
|
:title="$t('menus.ai.plugin')"
|
||||||
width="730"
|
width="730"
|
||||||
ref="pluginSelectRef"
|
ref="pluginSelectRef"
|
||||||
page-url="/api/v1/plugin/page"
|
page-url="/api/v1/plugin/page?availableOnly=true"
|
||||||
:has-parent="true"
|
:has-parent="true"
|
||||||
single-select
|
single-select
|
||||||
@get-data="(v) => handleChoose(nodeNames.pluginNode, v)"
|
@get-data="(v) => handleChoose(nodeNames.pluginNode, v)"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
CardPrimaryAction,
|
CardPrimaryAction,
|
||||||
} from '#/components/page/CardList.vue';
|
} from '#/components/page/CardList.vue';
|
||||||
|
|
||||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
import { computed, h, markRaw, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@easyflow/access';
|
import { useAccess } from '@easyflow/access';
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
@@ -360,14 +360,48 @@ async function submitOfflineAction(row: any) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
const sections = [];
|
||||||
impactRes.data?.hasBotBindings
|
if (impactRes.data?.hasBotBindings) {
|
||||||
? buildOfflineImpactMessage(
|
sections.push(
|
||||||
|
buildOfflineImpactMessage(
|
||||||
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
|
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
|
||||||
impactRes.data.botBindings,
|
impactRes.data.botBindings,
|
||||||
$t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
impactRes.data?.hasPluginBindings
|
||||||
)
|
? undefined
|
||||||
: $t('aiWorkflow.submitOfflineApprovalConfirm'),
|
: $t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (impactRes.data?.hasPluginBindings) {
|
||||||
|
sections.push(
|
||||||
|
buildOfflineImpactMessage(
|
||||||
|
$t('aiWorkflow.offlineImpactBoundPluginsIntro'),
|
||||||
|
impactRes.data.pluginBindings,
|
||||||
|
impactRes.data?.hasBotBindings
|
||||||
|
? undefined
|
||||||
|
: $t('aiWorkflow.offlineImpactBoundPluginsFooter'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const impactMessage =
|
||||||
|
sections.length > 0
|
||||||
|
? h('div', [
|
||||||
|
...sections,
|
||||||
|
h(
|
||||||
|
'p',
|
||||||
|
{
|
||||||
|
style: 'margin-top: 12px;',
|
||||||
|
},
|
||||||
|
impactRes.data?.hasBotBindings && impactRes.data?.hasPluginBindings
|
||||||
|
? $t('aiWorkflow.offlineImpactBoundMixedFooter')
|
||||||
|
: impactRes.data?.hasPluginBindings
|
||||||
|
? $t('aiWorkflow.offlineImpactBoundPluginsFooter')
|
||||||
|
: $t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
: $t('aiWorkflow.submitOfflineApprovalConfirm');
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
impactMessage,
|
||||||
$t('message.noticeTitle'),
|
$t('message.noticeTitle'),
|
||||||
{
|
{
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import type { FormInstance } from 'element-plus';
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { Position } from '@element-plus/icons-vue';
|
import { Position } from '@element-plus/icons-vue';
|
||||||
import { ElButton, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
import { ElButton, ElForm, ElFormItem, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import ShowJson from '#/components/json/ShowJson.vue';
|
import ShowJson from '#/components/json/ShowJson.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import WorkflowFormItem from '#/views/ai/workflow/components/WorkflowFormItem.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workflowId: any;
|
workflowId: any;
|
||||||
@@ -47,19 +48,10 @@ function submit() {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ElForm label-position="top" ref="singleRunForm" :model="runParams">
|
<ElForm label-position="top" ref="singleRunForm" :model="runParams">
|
||||||
<ElFormItem
|
<WorkflowFormItem
|
||||||
v-for="(item, idx) in node?.data.parameters"
|
v-model:run-params="runParams"
|
||||||
:prop="item.name"
|
:parameters="node?.data.parameters || []"
|
||||||
:key="idx"
|
|
||||||
:label="item.description || item.name"
|
|
||||||
:rules="[{ required: true, message: $t('message.required') }]"
|
|
||||||
>
|
|
||||||
<ElInput
|
|
||||||
v-if="item.formType === 'input' || !item.formType"
|
|
||||||
v-model="runParams[item.name]"
|
|
||||||
:placeholder="item.formPlaceholder"
|
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem>
|
<ElFormItem>
|
||||||
<ElButton
|
<ElButton
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@@ -29,16 +29,64 @@ export interface WorkflowStepsProps {
|
|||||||
nodeJson: any;
|
nodeJson: any;
|
||||||
initSignal?: boolean;
|
initSignal?: boolean;
|
||||||
pollingData?: any;
|
pollingData?: any;
|
||||||
|
expandAll?: boolean;
|
||||||
}
|
}
|
||||||
const props = defineProps<WorkflowStepsProps>();
|
const props = defineProps<WorkflowStepsProps>();
|
||||||
const emit = defineEmits(['resume']);
|
const emit = defineEmits(['resume']);
|
||||||
const nodes = ref<any[]>([]);
|
const nodes = ref<any[]>([]);
|
||||||
const nodeStatusMap = ref<Record<string, any>>({});
|
const nodeStatusMap = ref<Record<string, any>>({});
|
||||||
const isChainError = ref(false);
|
const isChainError = ref(false);
|
||||||
|
const activeName = ref<any>(props.expandAll ? [] : '');
|
||||||
|
const confirmParams = ref<any>({});
|
||||||
|
// 定义一个对象来存储所有的 form 实例,key 为 node.key
|
||||||
|
const formRefs = ref<Record<string, FormInstance>>({});
|
||||||
|
const confirmBtnLoading = ref(false);
|
||||||
|
const chainErrMsg = ref('');
|
||||||
|
|
||||||
|
function shouldAutoExpandStatus(status: unknown) {
|
||||||
|
return [1, 5, 20, 21].includes(Number(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpandedNode(nodeKey: string) {
|
||||||
|
if (Array.isArray(activeName.value)) {
|
||||||
|
return activeName.value.includes(nodeKey);
|
||||||
|
}
|
||||||
|
return activeName.value === nodeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNodePayloadChanged(previous: any, current: any) {
|
||||||
|
if (previous === current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return JSON.stringify(previous ?? null) !== JSON.stringify(current ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNodeStateChanged(previous: any, current: any) {
|
||||||
|
if (!previous) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((previous?.status || null) !== (current?.status || null)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((previous?.message || '') !== (current?.message || '')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasNodePayloadChanged(previous?.result, current?.result)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasNodePayloadChanged(
|
||||||
|
previous?.suspendForParameters,
|
||||||
|
current?.suspendForParameters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.pollingData,
|
() => props.pollingData,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
const nodes = newVal.nodes;
|
if (!newVal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentNodes = newVal.nodes || {};
|
||||||
if (newVal.status === 21) {
|
if (newVal.status === 21) {
|
||||||
isChainError.value = true;
|
isChainError.value = true;
|
||||||
chainErrMsg.value = newVal.message;
|
chainErrMsg.value = newVal.message;
|
||||||
@@ -46,9 +94,20 @@ watch(
|
|||||||
if (![20, 21].includes(newVal.status)) {
|
if (![20, 21].includes(newVal.status)) {
|
||||||
confirmBtnLoading.value = false;
|
confirmBtnLoading.value = false;
|
||||||
}
|
}
|
||||||
for (const nodeId in nodes) {
|
for (const nodeId in currentNodes) {
|
||||||
nodeStatusMap.value[nodeId] = nodes[nodeId];
|
const previousNodeState = nodeStatusMap.value[nodeId];
|
||||||
if (nodes[nodeId].status === 5) {
|
const currentNodeState = currentNodes[nodeId];
|
||||||
|
const previousStatus = previousNodeState?.status;
|
||||||
|
const currentStatus = currentNodeState?.status;
|
||||||
|
if (!hasNodeStateChanged(previousNodeState, currentNodeState)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nodeStatusMap.value[nodeId] = currentNodeState;
|
||||||
|
if (
|
||||||
|
!props.expandAll &&
|
||||||
|
previousStatus !== currentStatus &&
|
||||||
|
shouldAutoExpandStatus(currentStatus)
|
||||||
|
) {
|
||||||
activeName.value = nodeId;
|
activeName.value = nodeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,14 +121,17 @@ watch(
|
|||||||
isChainError.value = false;
|
isChainError.value = false;
|
||||||
confirmBtnLoading.value = false;
|
confirmBtnLoading.value = false;
|
||||||
chainErrMsg.value = '';
|
chainErrMsg.value = '';
|
||||||
|
activeName.value = props.expandAll ? [] : '';
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
watch(
|
watch(
|
||||||
() => props.nodeJson,
|
() => props.nodeJson,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal) {
|
const nextNodes = Array.isArray(newVal) ? [...newVal] : [];
|
||||||
nodes.value = [...newVal];
|
nodes.value = nextNodes;
|
||||||
}
|
activeName.value = props.expandAll
|
||||||
|
? nextNodes.map((node: any) => node.key)
|
||||||
|
: '';
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -79,18 +141,12 @@ const displayNodes = computed(() => {
|
|||||||
...nodeStatusMap.value[node.key],
|
...nodeStatusMap.value[node.key],
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
const activeName = ref('1');
|
|
||||||
const confirmParams = ref<any>({});
|
|
||||||
// 定义一个对象来存储所有的 form 实例,key 为 node.key
|
|
||||||
const formRefs = ref<Record<string, FormInstance>>({});
|
|
||||||
// 动态设置 Ref 的辅助函数
|
// 动态设置 Ref 的辅助函数
|
||||||
const setFormRef = (el: any, key: string) => {
|
const setFormRef = (el: any, key: string) => {
|
||||||
if (el) {
|
if (el) {
|
||||||
formRefs.value[key] = el as FormInstance;
|
formRefs.value[key] = el as FormInstance;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const confirmBtnLoading = ref(false);
|
|
||||||
const chainErrMsg = ref('');
|
|
||||||
function getSelectMode(ops: any) {
|
function getSelectMode(ops: any) {
|
||||||
return ops.formType || 'radio';
|
return ops.formType || 'radio';
|
||||||
}
|
}
|
||||||
@@ -124,7 +180,11 @@ function handleConfirm(node: any) {
|
|||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<ElAlert v-if="chainErrMsg" :title="chainErrMsg" type="error" />
|
<ElAlert v-if="chainErrMsg" :title="chainErrMsg" type="error" />
|
||||||
</div>
|
</div>
|
||||||
<ElCollapse v-model="activeName" accordion expand-icon-position="left">
|
<ElCollapse
|
||||||
|
v-model="activeName"
|
||||||
|
:accordion="!props.expandAll"
|
||||||
|
expand-icon-position="left"
|
||||||
|
>
|
||||||
<ElCollapseItem
|
<ElCollapseItem
|
||||||
v-for="node in displayNodes"
|
v-for="node in displayNodes"
|
||||||
:key="node.key"
|
:key="node.key"
|
||||||
@@ -165,6 +225,7 @@ function handleConfirm(node: any) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="isExpandedNode(node.key)">
|
||||||
<div v-if="node.original.type === 'confirmNode'" class="p-2.5">
|
<div v-if="node.original.type === 'confirmNode'" class="p-2.5">
|
||||||
<div class="mb-2 text-[16px] font-bold">
|
<div class="mb-2 text-[16px] font-bold">
|
||||||
{{ node.original.data.message }}
|
{{ node.original.data.message }}
|
||||||
@@ -223,6 +284,7 @@ function handleConfirm(node: any) {
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<ShowJson :value="node.result || node.message" />
|
<ShowJson :value="node.result || node.message" />
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</ElCollapseItem>
|
</ElCollapseItem>
|
||||||
</ElCollapse>
|
</ElCollapse>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
let inputEl = $state<HTMLInputElement | null>(null);
|
let inputEl = $state<HTMLInputElement | null>(null);
|
||||||
let textareaEl = $state<HTMLTextAreaElement | null>(null);
|
let textareaEl = $state<HTMLTextAreaElement | null>(null);
|
||||||
let highlightEl = $state<HTMLDivElement | null>(null);
|
let highlightEl = $state<HTMLDivElement | null>(null);
|
||||||
|
let scrollbarWidth = $state(0);
|
||||||
let triggerObject: any;
|
let triggerObject: any;
|
||||||
let isFocused = $state(false);
|
let isFocused = $state(false);
|
||||||
let isComposing = $state(false);
|
let isComposing = $state(false);
|
||||||
@@ -49,6 +50,17 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
localValue;
|
||||||
|
if (mode !== 'textarea') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
updateScrollbarWidth();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const paramCandidates = $derived(flattenParameterCandidates(parameters));
|
const paramCandidates = $derived(flattenParameterCandidates(parameters));
|
||||||
const paramNames = $derived(paramCandidates.map((item) => item.name));
|
const paramNames = $derived(paramCandidates.map((item) => item.name));
|
||||||
const unresolvedParamSet = $derived.by(() => {
|
const unresolvedParamSet = $derived.by(() => {
|
||||||
@@ -129,9 +141,48 @@
|
|||||||
highlightEl.scrollLeft = el.scrollLeft;
|
highlightEl.scrollLeft = el.scrollLeft;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateScrollbarWidth = () => {
|
||||||
|
const el = textareaEl;
|
||||||
|
if (!el || mode !== 'textarea') {
|
||||||
|
scrollbarWidth = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
const borderLeft = parseFloat(style.borderLeftWidth || '0');
|
||||||
|
const borderRight = parseFloat(style.borderRightWidth || '0');
|
||||||
|
const nextWidth = Math.max(0, el.offsetWidth - el.clientWidth - borderLeft - borderRight);
|
||||||
|
scrollbarWidth = Number.isFinite(nextWidth) ? nextWidth : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncEditorMetrics = () => {
|
||||||
|
syncScroll();
|
||||||
|
updateScrollbarWidth();
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (mode !== 'textarea' || !textareaEl) {
|
||||||
|
scrollbarWidth = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = typeof ResizeObserver !== 'undefined'
|
||||||
|
? new ResizeObserver(() => {
|
||||||
|
updateScrollbarWidth();
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
updateScrollbarWidth();
|
||||||
|
resizeObserver?.observe(textareaEl);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const handleInput = (event: Event) => {
|
const handleInput = (event: Event) => {
|
||||||
localValue = ((event.target as HTMLInputElement | HTMLTextAreaElement).value || '') as string;
|
localValue = ((event.target as HTMLInputElement | HTMLTextAreaElement).value || '') as string;
|
||||||
syncScroll();
|
syncEditorMetrics();
|
||||||
emitInput(event);
|
emitInput(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,7 +209,7 @@
|
|||||||
newEditorEl.focus();
|
newEditorEl.focus();
|
||||||
newEditorEl.setSelectionRange(result.cursor, result.cursor);
|
newEditorEl.setSelectionRange(result.cursor, result.cursor);
|
||||||
}
|
}
|
||||||
syncScroll();
|
syncEditorMetrics();
|
||||||
triggerObject?.hide?.();
|
triggerObject?.hide?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,7 +361,7 @@
|
|||||||
editorEl.focus();
|
editorEl.focus();
|
||||||
editorEl.setSelectionRange(deleteRange.start, deleteRange.start);
|
editorEl.setSelectionRange(deleteRange.start, deleteRange.start);
|
||||||
}
|
}
|
||||||
syncScroll();
|
syncEditorMetrics();
|
||||||
rest.onkeydown?.(event);
|
rest.onkeydown?.(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -339,7 +390,7 @@
|
|||||||
editorEl.focus();
|
editorEl.focus();
|
||||||
editorEl.setSelectionRange(tokenRange.start, tokenRange.start);
|
editorEl.setSelectionRange(tokenRange.start, tokenRange.start);
|
||||||
}
|
}
|
||||||
syncScroll();
|
syncEditorMetrics();
|
||||||
rest.onkeydown?.(event);
|
rest.onkeydown?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -400,6 +451,7 @@
|
|||||||
<div
|
<div
|
||||||
class="param-token-editor-highlight {mode === 'input' ? 'single-line' : 'multi-line'}"
|
class="param-token-editor-highlight {mode === 'input' ? 'single-line' : 'multi-line'}"
|
||||||
bind:this={highlightEl}
|
bind:this={highlightEl}
|
||||||
|
style={`--param-token-scrollbar-width: ${scrollbarWidth}px;`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{@html highlightedHtml}
|
{@html highlightedHtml}
|
||||||
@@ -571,7 +623,9 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: var(--param-token-padding-y) var(--param-token-padding-right) var(--param-token-padding-y)
|
padding: var(--param-token-padding-y)
|
||||||
|
calc(var(--param-token-padding-right) + var(--param-token-scrollbar-width, 0px))
|
||||||
|
var(--param-token-padding-y)
|
||||||
var(--param-token-padding-left);
|
var(--param-token-padding-left);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--tf-text-primary);
|
color: var(--tf-text-primary);
|
||||||
@@ -621,6 +675,8 @@
|
|||||||
|
|
||||||
.param-token-textarea {
|
.param-token-textarea {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
display: block;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-token-action {
|
.param-token-action {
|
||||||
|
|||||||
Reference in New Issue
Block a user