feat: 完善用户确认节点选项与输出流转

- 重构确认节点单选多选配置及输出参数契约

- 统一管理端、用户中心、匿名分享和公共接口恢复流程

- 增加保存校验、错误契约及交互测试
This commit is contained in:
2026-09-04 14:55:55 +08:00
parent b99e275371
commit c7cac61ce8
51 changed files with 2465 additions and 1591 deletions

View File

@@ -18,10 +18,12 @@ 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.service.WorkflowResumeService;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.service.PluginService;
@@ -29,6 +31,7 @@ import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.service.WorkflowExecResultService;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
@@ -91,11 +94,15 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
@Resource
private WorkflowService workflowService;
@Resource
private WorkflowExecResultService workflowExecResultService;
@Resource
private ChainExecutor chainExecutor;
@Resource
private TinyFlowService tinyFlowService;
@Resource
private WorkflowCheckService workflowCheckService;
@Resource
private WorkflowResumeService workflowResumeService;
@PostMapping("/tool/save")
@SaCheckPermission("/api/v1/plugin/save")
@@ -215,6 +222,7 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
@SaCheckPermission("/api/v1/plugin/query")
public Result<ChainInfo> pluginToolTestChainStatus(@JsonBody(value = "executeId", required = true) String executeId,
@JsonBody("nodes") List<NodeInfo> nodes) {
assertPluginTestExecutionOwnership(executeId);
return Result.ok(tinyFlowService.getChainStatus(executeId, nodes));
}
@@ -229,10 +237,33 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
@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);
assertPluginTestExecutionOwnership(executeId);
workflowResumeService.resume(executeId, confirmParams);
return Result.ok();
}
/**
* 校验插件试运行实例由当前登录用户发起。
*
* @param executeId 执行实例 ID
*/
private void assertPluginTestExecutionOwnership(String executeId) {
if (StrUtil.isBlank(executeId)) {
throw new BusinessException("执行ID不能为空");
}
WorkflowExecResult record = workflowExecResultService.getByExecKey(executeId);
if (record == null) {
throw new BusinessException(404, 404, "工作流执行记录不存在或已过期");
}
LoginAccount currentAccount = SaTokenUtil.getLoginAccount();
if (currentAccount == null
|| currentAccount.getId() == null
|| record.getCreatedBy() == null
|| !currentAccount.getId().toString().equals(record.getCreatedBy())) {
throw new BusinessException(403, 403, "无权限访问当前插件试运行实例");
}
}
private void handleArray(JSONArray array) {
for (Object o : array) {
JSONObject obj = (JSONObject) o;

View File

@@ -1,6 +1,5 @@
package tech.easyflow.admin.controller.ai;
import com.easyagents.flow.core.chain.ChainStatus;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
@@ -14,6 +13,7 @@ import tech.easyflow.admin.service.ai.WorkflowChatEventStream;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.entity.WorkflowExecResult;
@@ -64,6 +64,8 @@ public class WorkflowChatController {
@Resource
private ChainExecutor chainExecutor;
@Resource
private WorkflowResumeService workflowResumeService;
@Resource
private WorkflowExecResultService execResultService;
@Resource
private WorkflowExecStepService execStepService;
@@ -171,19 +173,8 @@ public class WorkflowChatController {
@JsonBody("confirmParams")
Map<String, Object> confirmParams
) {
WorkflowExecResult record = assertExecutionOwnership(executeId);
if (record.getStatus() != null
&& (record.getStatus() == ChainStatus.SUCCEEDED.getValue()
|| record.getStatus() == ChainStatus.FAILED.getValue()
|| record.getStatus() == ChainStatus.CANCELLED.getValue())) {
throw new BusinessException("当前工作流执行已结束");
}
chainExecutor.resumeAsync(
executeId,
confirmParams == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(confirmParams)
);
assertExecutionOwnership(executeId);
workflowResumeService.resume(executeId, confirmParams);
return Result.ok();
}

View File

@@ -31,6 +31,7 @@ import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.publish.WorkflowPublishAppService;
@@ -94,6 +95,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
@Resource
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
@Resource
private WorkflowResumeService workflowResumeService;
@Resource
private ResourceAccessService resourceAccessService;
@Resource
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
@@ -324,12 +327,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
)
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
if (!chainExecutor.resumeAsyncIfSuspended(executeId, confirmParams)) {
throw new BusinessException(
409,
40901,
"当前执行状态不可恢复,仅暂停中的工作流允许恢复");
}
workflowResumeService.resume(executeId, confirmParams);
return Result.ok();
}

View File

@@ -13,6 +13,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.entity.WorkflowExecStep;
@@ -46,6 +47,7 @@ public class WorkflowPublicChatService {
private final WorkflowPublicChatAccessGuard accessGuard;
private final WorkflowChatEventStream eventStream;
private final ChainExecutor chainExecutor;
private final WorkflowResumeService workflowResumeService;
private final WorkflowExecResultService execResultService;
private final WorkflowExecStepService execStepService;
@@ -57,6 +59,7 @@ public class WorkflowPublicChatService {
WorkflowPublicChatAccessGuard accessGuard,
WorkflowChatEventStream eventStream,
ChainExecutor chainExecutor,
WorkflowResumeService workflowResumeService,
WorkflowExecResultService execResultService,
WorkflowExecStepService execStepService
) {
@@ -67,6 +70,7 @@ public class WorkflowPublicChatService {
this.accessGuard = accessGuard;
this.eventStream = eventStream;
this.chainExecutor = chainExecutor;
this.workflowResumeService = workflowResumeService;
this.execResultService = execResultService;
this.execStepService = execStepService;
}
@@ -185,17 +189,8 @@ public class WorkflowPublicChatService {
) {
WorkflowPublicChatContext context = contextResolver.resolveActive(
shareKey, visitorId);
WorkflowExecResult record = assertExecutionOwnership(
context, executeId);
if (isTerminal(record.getStatus())) {
throw new BusinessException("当前工作流执行已结束");
}
chainExecutor.resumeAsync(
executeId,
confirmParams == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(confirmParams)
);
assertExecutionOwnership(context, executeId);
workflowResumeService.resume(executeId, confirmParams);
}
/**
@@ -253,13 +248,6 @@ public class WorkflowPublicChatService {
return record;
}
private boolean isTerminal(Integer status) {
return status != null
&& (status == ChainStatus.SUCCEEDED.getValue()
|| status == ChainStatus.FAILED.getValue()
|| status == ChainStatus.CANCELLED.getValue());
}
private Map<String, Object> buildExecutionDetail(
WorkflowExecResult record,
List<WorkflowExecStep> steps,

View File

@@ -7,21 +7,27 @@ import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.WorkflowExecResultService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
@@ -68,6 +74,61 @@ public class PluginItemControllerTest {
verify(visibilityService).assertPluginVisible(1L, BigInteger.TEN, "无权限删除该插件工具");
}
/**
* 验证当前用户不能恢复其他用户发起的插件试运行实例。
*/
@Test
public void testResumeShouldRejectAnotherUsersExecution() {
PluginItemService pluginItemService = mock(PluginItemService.class);
WorkflowExecResultService execResultService = mock(WorkflowExecResultService.class);
WorkflowResumeService resumeService = mock(WorkflowResumeService.class);
WorkflowExecResult record = new WorkflowExecResult();
record.setCreatedBy(BigInteger.ONE.toString());
when(execResultService.getByExecKey("execution-1")).thenReturn(record);
PluginItemController controller = new PluginItemController(pluginItemService);
setField(controller, "workflowExecResultService", execResultService);
setField(controller, "workflowResumeService", resumeService);
LoginAccount currentAccount = new LoginAccount();
currentAccount.setId(BigInteger.TWO);
try (MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class)) {
login.when(SaTokenUtil::getLoginAccount).thenReturn(currentAccount);
BusinessException error = Assert.expectThrows(
BusinessException.class,
() -> controller.pluginToolTestResume("execution-1", Map.of())
);
Assert.assertEquals(error.getHttpStatus(), 403);
Assert.assertEquals(error.getErrorCode(), 403);
}
verifyNoInteractions(resumeService);
}
/**
* 验证当前用户可以恢复自己发起的插件试运行实例。
*/
@Test
public void testResumeShouldAllowExecutionOwner() {
PluginItemService pluginItemService = mock(PluginItemService.class);
WorkflowExecResultService execResultService = mock(WorkflowExecResultService.class);
WorkflowResumeService resumeService = mock(WorkflowResumeService.class);
WorkflowExecResult record = new WorkflowExecResult();
record.setCreatedBy(BigInteger.ONE.toString());
when(execResultService.getByExecKey("execution-1")).thenReturn(record);
PluginItemController controller = new PluginItemController(pluginItemService);
setField(controller, "workflowExecResultService", execResultService);
setField(controller, "workflowResumeService", resumeService);
LoginAccount currentAccount = new LoginAccount();
currentAccount.setId(BigInteger.ONE);
try (MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class)) {
login.when(SaTokenUtil::getLoginAccount).thenReturn(currentAccount);
controller.pluginToolTestResume("execution-1", Map.of("choice", "A"));
}
verify(resumeService).resume("execution-1", Map.of("choice", "A"));
}
/**
* 创建插件工具。
*

View File

@@ -12,6 +12,7 @@ import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.entity.WorkflowExecResult;
@@ -162,6 +163,8 @@ public class WorkflowPublicChatServiceTest {
WorkflowChatEventStream eventStream = mock(
WorkflowChatEventStream.class);
ChainExecutor chainExecutor = mock(ChainExecutor.class);
WorkflowResumeService workflowResumeService =
mock(WorkflowResumeService.class);
WorkflowExecResultService execResultService = mock(
WorkflowExecResultService.class);
WorkflowExecStepService execStepService = mock(
@@ -196,6 +199,7 @@ public class WorkflowPublicChatServiceTest {
accessGuard,
eventStream,
chainExecutor,
workflowResumeService,
execResultService,
execStepService
);