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

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

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

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

View File

@@ -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;
}
}