From 0968e3bfa5f4fb7671a632b21f26ef55727c6c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Fri, 4 Sep 2026 14:55:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E8=8A=82=E7=82=B9=E9=80=89=E9=A1=B9=E4=B8=8E?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E6=B5=81=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构确认节点单选多选配置及输出参数契约 - 统一管理端、用户中心、匿名分享和公共接口恢复流程 - 增加保存校验、错误契约及交互测试 --- .../controller/ai/PluginItemController.java | 33 +- .../controller/ai/WorkflowChatController.java | 19 +- .../controller/ai/WorkflowController.java | 10 +- .../service/ai/WorkflowPublicChatService.java | 24 +- .../ai/PluginItemControllerTest.java | 61 ++ .../ai/WorkflowPublicChatServiceTest.java | 4 + .../controller/PublicWorkflowController.java | 12 +- .../error/WorkflowRunAsyncErrorProfile.java | 4 +- .../PublicWorkflowControllerBehaviorTest.java | 19 +- .../WorkflowRunAsyncErrorProfileTest.java | 22 + .../controller/ai/UcWorkflowController.java | 10 +- .../service/TinyFlowService.java | 7 +- .../service/WorkflowCheckService.java | 318 +++++++++++ .../service/WorkflowResumeService.java | 49 ++ .../service/TinyFlowServiceTest.java | 51 ++ .../service/WorkflowCheckServiceTest.java | 395 +++++++++++++ .../service/WorkflowResumeServiceTest.java | 62 ++ .../views/ai/plugin/PluginRunTestModal.vue | 28 +- .../src/views/ai/workflow/WorkflowDesign.vue | 25 +- .../src/views/ai/workflow/WorkflowList.vue | 9 +- .../ai/workflow/components/ConfirmItem.vue | 211 ------- .../workflow/components/ConfirmItemMulti.vue | 216 ------- .../workflow/components/WorkflowChatPage.vue | 51 +- .../ai/workflow/components/WorkflowForm.vue | 16 +- .../workflow/components/WorkflowFormItem.vue | 6 + .../ai/workflow/components/WorkflowSteps.vue | 134 ++--- .../__tests__/WorkflowSteps.test.ts | 168 +++++- .../__tests__/workflowPublicFormItem.test.ts | 29 + .../components/workflowFormParameters.ts | 8 +- .../packages/tinyflow-ui/src/Tinyflow.ts | 7 +- .../src/components/base/select.svelte | 156 +++-- .../core/ConfirmParameterItem.svelte | 177 ------ .../core/ConfirmParameterList.svelte | 66 --- .../src/components/core/NodeWrapper.svelte | 22 +- .../src/components/core/OutputDefItem.svelte | 107 ++-- .../src/components/core/OutputDefList.svelte | 18 +- .../src/components/nodes/ConfirmNode.svelte | 539 ++++++++++++++---- .../src/components/utils/confirmNode.test.ts | 131 +++++ .../src/components/utils/confirmNode.ts | 102 ++++ .../src/components/utils/nodePalette.ts | 7 + .../tinyflow-ui/src/store/stores.svelte.ts | 8 + .../tinyflow-ui/src/styles/tinyflow.less | 6 + .../src/tinyflow-store-isolation.test.ts | 31 + .../packages/tinyflow-ui/src/types.ts | 8 + .../src/utils/workflowNodeFields.test.ts | 63 +- .../src/utils/workflowNodeFields.ts | 20 +- .../ai/workflow/components/ConfirmItem.vue | 210 ------- .../workflow/components/ConfirmItemMulti.vue | 216 ------- .../ai/workflow/components/WorkflowForm.vue | 16 +- .../workflow/components/WorkflowFormItem.vue | 10 +- .../ai/workflow/components/WorkflowSteps.vue | 135 ++--- 51 files changed, 2465 insertions(+), 1591 deletions(-) create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowResumeService.java create mode 100644 easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowResumeServiceTest.java delete mode 100644 easyflow-ui-admin/app/src/views/ai/workflow/components/ConfirmItem.vue delete mode 100644 easyflow-ui-admin/app/src/views/ai/workflow/components/ConfirmItemMulti.vue delete mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ConfirmParameterItem.svelte delete mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ConfirmParameterList.svelte create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/confirmNode.test.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/confirmNode.ts delete mode 100644 easyflow-ui-usercenter/app/src/views/ai/workflow/components/ConfirmItem.vue delete mode 100644 easyflow-ui-usercenter/app/src/views/ai/workflow/components/ConfirmItemMulti.vue diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginItemController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginItemController.java index 30e898e6..c848cb79 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginItemController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginItemController.java @@ -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 pluginToolTestChainStatus(@JsonBody(value = "executeId", required = true) String executeId, @JsonBody("nodes") List nodes) { + assertPluginTestExecutionOwnership(executeId); return Result.ok(tinyFlowService.getChainStatus(executeId, nodes)); } @@ -229,10 +237,33 @@ public class PluginItemController extends BaseCurdController pluginToolTestResume(@JsonBody(value = "executeId", required = true) String executeId, @JsonBody("confirmParams") Map 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; diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowChatController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowChatController.java index e2091282..36450a4f 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowChatController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowChatController.java @@ -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 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(); } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java index b8257f5c..c5b3d916 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java @@ -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 resume(@JsonBody(value = "executeId", required = true) String executeId, @JsonBody("confirmParams") Map confirmParams) { - if (!chainExecutor.resumeAsyncIfSuspended(executeId, confirmParams)) { - throw new BusinessException( - 409, - 40901, - "当前执行状态不可恢复,仅暂停中的工作流允许恢复"); - } + workflowResumeService.resume(executeId, confirmParams); return Result.ok(); } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/service/ai/WorkflowPublicChatService.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/service/ai/WorkflowPublicChatService.java index da618e33..7d3fc1cc 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/service/ai/WorkflowPublicChatService.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/service/ai/WorkflowPublicChatService.java @@ -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 buildExecutionDetail( WorkflowExecResult record, List steps, diff --git a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/PluginItemControllerTest.java b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/PluginItemControllerTest.java index a63dc5b8..6d65e34d 100644 --- a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/PluginItemControllerTest.java +++ b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/PluginItemControllerTest.java @@ -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 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 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")); + } + /** * 创建插件工具。 * diff --git a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/service/ai/WorkflowPublicChatServiceTest.java b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/service/ai/WorkflowPublicChatServiceTest.java index a6823b25..7f8a1ca2 100644 --- a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/service/ai/WorkflowPublicChatServiceTest.java +++ b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/service/ai/WorkflowPublicChatServiceTest.java @@ -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 ); diff --git a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java index b6b20f01..14f383d4 100644 --- a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java +++ b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java @@ -18,6 +18,7 @@ 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.WorkflowRunningParameterResolver; +import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService; import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds; import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiPreparedUpload; import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiUploadLifecycleService; @@ -70,6 +71,8 @@ public class PublicWorkflowController { @Resource private WorkflowRunningParameterResolver workflowRunningParameterResolver; @Resource + private WorkflowResumeService workflowResumeService; + @Resource private WorkflowApiPermissionService workflowApiPermissionService; @Resource private WorkflowExecResultService workflowExecResultService; @@ -250,14 +253,7 @@ public class PublicWorkflowController { SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI()); WorkflowExecResult execResult = assertApiKeyExecutionOwnership(apiKey, executeId); assertWorkflowExecutionResumable(execResult); - if (!chainExecutor.resumeAsyncIfSuspended( - executeId, - confirmParams)) { - throw new BusinessException( - 409, - 40901, - "当前执行状态不可恢复,仅暂停中的工作流允许恢复"); - } + workflowResumeService.resume(executeId, confirmParams); return Result.ok(); } diff --git a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/error/WorkflowRunAsyncErrorProfile.java b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/error/WorkflowRunAsyncErrorProfile.java index 03fc31af..f5fa1a65 100644 --- a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/error/WorkflowRunAsyncErrorProfile.java +++ b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/error/WorkflowRunAsyncErrorProfile.java @@ -394,6 +394,7 @@ public final class WorkflowRunAsyncErrorProfile */ private boolean isStableBusinessCode(int code) { return (code >= 40011 && code <= 40017) + || code == 40031 || (code >= 40101 && code <= 40103) || (code >= 40301 && code <= 40302) || (code >= 40401 && code <= 40402) @@ -412,7 +413,8 @@ public final class WorkflowRunAsyncErrorProfile * @return 对外 HTTP 状态 */ private int normalizeHttpStatus(int code, int fallback) { - if (code >= 40011 && code <= 40017) { + if ((code >= 40011 && code <= 40017) + || code == 40031) { return 400; } if (code >= 40101 && code <= 40103) { diff --git a/easyflow-api/easyflow-api-public/src/test/java/tech/easyflow/publicapi/controller/PublicWorkflowControllerBehaviorTest.java b/easyflow-api/easyflow-api-public/src/test/java/tech/easyflow/publicapi/controller/PublicWorkflowControllerBehaviorTest.java index 8cecbf26..247fa2c0 100644 --- a/easyflow-api/easyflow-api-public/src/test/java/tech/easyflow/publicapi/controller/PublicWorkflowControllerBehaviorTest.java +++ b/easyflow-api/easyflow-api-public/src/test/java/tech/easyflow/publicapi/controller/PublicWorkflowControllerBehaviorTest.java @@ -12,6 +12,7 @@ import org.springframework.test.util.ReflectionTestUtils; import tech.easyflow.ai.easyagentsflow.entity.ChainInfo; import tech.easyflow.ai.easyagentsflow.entity.NodeInfo; import tech.easyflow.ai.easyagentsflow.service.TinyFlowService; +import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService; import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.entity.WorkflowExecResult; import tech.easyflow.ai.enums.PublishStatus; @@ -44,6 +45,7 @@ public class PublicWorkflowControllerBehaviorTest { private PublicWorkflowController controller; private ChainExecutor chainExecutor; + private WorkflowResumeService workflowResumeService; private TinyFlowService tinyFlowService; private HttpServletRequest request; @@ -54,6 +56,7 @@ public class PublicWorkflowControllerBehaviorTest { public void setUp() { controller = new PublicWorkflowController(); chainExecutor = Mockito.mock(ChainExecutor.class); + workflowResumeService = Mockito.mock(WorkflowResumeService.class); tinyFlowService = Mockito.mock(TinyFlowService.class); WorkflowApiPermissionService permissionService = Mockito.mock(WorkflowApiPermissionService.class); @@ -79,6 +82,10 @@ public class PublicWorkflowControllerBehaviorTest { controller, "chainExecutor", chainExecutor); + ReflectionTestUtils.setField( + controller, + "workflowResumeService", + workflowResumeService); ReflectionTestUtils.setField( controller, "tinyFlowService", @@ -108,10 +115,12 @@ public class PublicWorkflowControllerBehaviorTest { public void resumeShouldRejectNonSuspendedExecution() { when(request.getRequestURI()).thenReturn( "/public-api/workflow/resume"); - when(chainExecutor.resumeAsyncIfSuspended( - EXECUTE_ID, - Map.of("approved", true))) - .thenReturn(false); + Mockito.doThrow(new BusinessException( + 409, + 40901, + "当前执行状态不可恢复,仅暂停中的工作流允许恢复")) + .when(workflowResumeService) + .resume(EXECUTE_ID, Map.of("approved", true)); try { controller.resume( @@ -124,7 +133,7 @@ public class PublicWorkflowControllerBehaviorTest { Assert.assertEquals(40901, exception.getErrorCode()); } - verify(chainExecutor).resumeAsyncIfSuspended( + verify(workflowResumeService).resume( EXECUTE_ID, Map.of("approved", true)); } diff --git a/easyflow-api/easyflow-api-public/src/test/java/tech/easyflow/publicapi/error/WorkflowRunAsyncErrorProfileTest.java b/easyflow-api/easyflow-api-public/src/test/java/tech/easyflow/publicapi/error/WorkflowRunAsyncErrorProfileTest.java index fcc1d3d0..ec334c20 100644 --- a/easyflow-api/easyflow-api-public/src/test/java/tech/easyflow/publicapi/error/WorkflowRunAsyncErrorProfileTest.java +++ b/easyflow-api/easyflow-api-public/src/test/java/tech/easyflow/publicapi/error/WorkflowRunAsyncErrorProfileTest.java @@ -167,6 +167,28 @@ public class WorkflowRunAsyncErrorProfileTest { resolution.modelAndView.getModel().get("message")); } + /** + * 验证确认节点恢复校验保留专用错误码,不回退为运行参数错误。 + */ + @Test + public void shouldKeepResumeValidationCode() { + Resolution resolution = resolve( + "/public-api/workflow/resume", + MediaType.APPLICATION_JSON_VALUE, + new BusinessException( + 400, + 40031, + "确认参数[模板类型]包含未配置选项")); + + Assert.assertEquals(400, resolution.response.getStatus()); + Assert.assertEquals( + 40031, + resolution.modelAndView.getModel().get("errorCode")); + Assert.assertEquals( + "确认参数[模板类型]包含未配置选项", + resolution.modelAndView.getModel().get("message")); + } + /** * 验证 API Key 无效和两层权限错误保持可区分。 */ diff --git a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcWorkflowController.java b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcWorkflowController.java index c26f0e5d..113dd912 100644 --- a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcWorkflowController.java +++ b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcWorkflowController.java @@ -13,6 +13,7 @@ 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.WorkflowRunningParameterResolver; +import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService; import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.service.WorkflowService; import tech.easyflow.common.annotation.UsePermission; @@ -54,6 +55,8 @@ public class UcWorkflowController extends BaseCurdController resume(@JsonBody(value = "executeId", required = true) String executeId, @JsonBody("confirmParams") Map confirmParams) { - if (!chainExecutor.resumeAsyncIfSuspended(executeId, confirmParams)) { - throw new BusinessException( - 409, - 40901, - "当前执行状态不可恢复,仅暂停中的工作流允许恢复"); - } + workflowResumeService.resume(executeId, confirmParams); return Result.ok(); } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowService.java index 819bf454..54128ed1 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowService.java @@ -142,9 +142,12 @@ public class TinyFlowService { node.setResult(resolved); } - // 只有当参数不为空时才覆盖 - if (chainState.getSuspendForParameters() != null) { + if (nodeState != null + && nodeState.getStatus() == NodeStatus.SUSPEND + && chainState.getSuspendForParameters() != null) { node.setSuspendForParameters(chainState.getSuspendForParameters()); + } else { + node.setSuspendForParameters(null); } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckService.java index 8211a68f..0be15574 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckService.java @@ -3,6 +3,8 @@ package tech.easyflow.ai.easyagentsflow.service; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; +import com.easyagents.flow.core.chain.DataType; +import com.easyagents.flow.core.node.ConfirmNode; import com.easyagents.flow.core.parser.ChainParser; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -53,6 +55,9 @@ public class WorkflowCheckService { private static final String TYPE_END = "endNode"; private static final String TYPE_LOOP = "loopNode"; private static final String TYPE_CONDITION = "conditionNode"; + private static final String TYPE_CONFIRM = "confirmNode"; + private static final Set CONFIRM_ARRAY_LEFT_OPERATORS = Set.of( + "contains", "notContains", "isEmpty", "isNotEmpty"); private static final String TYPE_WORKFLOW = "workflow-node"; private static final String TYPE_PLUGIN = "plugin-node"; private static final String TYPE_MAKE_FILE = "make-file"; @@ -184,6 +189,8 @@ public class WorkflowCheckService { } checkLoopConfigurations(nodes, nodeMap, issues, issueKeys); checkConditionConfigurations(nodes, issues, issueKeys); + checkConfirmConfigurations(nodes, issues, issueKeys); + checkConfirmOutputReferences(nodes, issues, issueKeys); List edges = new ArrayList<>(); Set edgeIds = new HashSet<>(); @@ -514,6 +521,288 @@ public class WorkflowCheckService { } } + /** + * 校验用户确认节点配置及其对外输出定义。 + */ + private void checkConfirmConfigurations( + List nodes, + List issues, + Set issueKeys) { + for (NodeView node : nodes) { + if (!TYPE_CONFIRM.equals(node.type)) { + continue; + } + if (node.data == null) { + addIssue( + issues, + issueKeys, + "CONFIRM_CONFIGURATION_INVALID", + "用户确认节点配置不能为空", + node.id, + null, + node.name); + continue; + } + + ConfirmNode configuration; + try { + validateConfirmConfigurationTypes(node.data); + checkConfirmOutputDefinitions( + node, + Boolean.TRUE.equals(node.data.get("multiple")), + issues, + issueKeys); + configuration = node.data.toJavaObject(ConfirmNode.class); + configuration.validateConfiguration(); + } catch (Exception exception) { + addIssue( + issues, + issueKeys, + "CONFIRM_CONFIGURATION_INVALID", + "用户确认节点配置无效: " + shortError(exception), + node.id, + null, + node.name); + continue; + } + } + } + + private void validateConfirmConfigurationTypes(JSONObject data) { + for (String key : data.keySet()) { + if (!ConfirmNode.SUPPORTED_CONFIGURATION_KEYS.contains(key)) { + throw new IllegalArgumentException( + "用户确认节点包含无效配置字段: " + key); + } + } + if (!(data.get("message") instanceof String)) { + throw new IllegalArgumentException("用户确认节点提示内容必须为字符串"); + } + if (!(data.get("multiple") instanceof Boolean)) { + throw new IllegalArgumentException("用户确认节点选择方式必须为布尔值"); + } + + Object optionsValue = data.get("options"); + if (optionsValue == null) { + return; + } + if (!(optionsValue instanceof JSONArray options)) { + throw new IllegalArgumentException("用户确认节点选项必须为数组"); + } + for (Object option : options) { + if (!(option instanceof String)) { + throw new IllegalArgumentException("用户确认节点选项内容必须为字符串"); + } + } + } + + private void checkConfirmOutputDefinitions( + NodeView node, + boolean multiple, + List issues, + Set issueKeys) { + Object outputDefsValue = node.data.get("outputDefs"); + if (!(outputDefsValue instanceof JSONArray outputDefs) + || outputDefs.size() != 1) { + addConfirmOutputIssue(node, issues, issueKeys); + return; + } + Object outputValue = outputDefs.get(0); + if (!(outputValue instanceof JSONObject output)) { + addConfirmOutputIssue(node, issues, issueKeys); + return; + } + String expectedType = multiple + ? DataType.Array_String.toString() + : DataType.String.toString(); + if (trimToNull(output.getString("name")) == null + || !expectedType.equals( + trimToNull(output.getString("dataType")))) { + addConfirmOutputIssue(node, issues, issueKeys); + } + } + + private void addConfirmOutputIssue( + NodeView node, + List issues, + Set issueKeys) { + addIssue( + issues, + issueKeys, + "CONFIRM_OUTPUT_SCHEMA_INVALID", + "用户确认节点必须配置唯一非空输出参数,且类型与选择方式一致", + node.id, + null, + node.name); + } + + /** + * 校验下游节点保存的确认输出引用仍然存在且类型一致。 + */ + private void checkConfirmOutputReferences( + List nodes, + List issues, + Set issueKeys) { + ConfirmOutputIndex confirmOutputs = new ConfirmOutputIndex(); + for (NodeView node : nodes) { + if (!TYPE_CONFIRM.equals(node.type) || node.data == null) { + continue; + } + try { + ConfirmNode configuration = node.data.toJavaObject(ConfirmNode.class); + configuration.validateConfiguration(); + confirmOutputs.put( + node.id, + configuration.resolveOutputName(), + configuration.isMultiple() + ? DataType.Array_String.toString() + : DataType.String.toString()); + } catch (Exception ignored) { + // 配置错误已由 checkConfirmConfigurations 给出精确问题。 + } + } + if (confirmOutputs.isEmpty()) { + return; + } + for (NodeView node : nodes) { + if (node.data != null) { + checkConfirmOutputReferences( + node.data, + node, + confirmOutputs, + issues, + issueKeys); + } + } + } + + private void checkConfirmOutputReferences( + Object value, + NodeView consumer, + ConfirmOutputIndex confirmOutputs, + List issues, + Set issueKeys) { + if (value instanceof JSONObject object) { + if ("ref".equals(trimToNull(object.getString("refType")))) { + checkConfirmOutputReference( + object.getString("ref"), + object.getString("dataType"), + true, + consumer, + confirmOutputs, + issues, + issueKeys); + } + String leftType = checkConfirmOutputReference( + object.getString("leftRef"), + null, + false, + consumer, + confirmOutputs, + issues, + issueKeys); + String operator = trimToNull(object.getString("operator")); + if (DataType.Array_String.toString().equals(leftType) + && !CONFIRM_ARRAY_LEFT_OPERATORS.contains(operator)) { + addConfirmConditionTypeIssue( + object.getString("leftRef"), + operator, + consumer, + issues, + issueKeys); + } + if ("ref".equals(trimToNull(object.getString("rightType")))) { + String rightType = checkConfirmOutputReference( + object.getString("rightRef"), + null, + false, + consumer, + confirmOutputs, + issues, + issueKeys); + if (DataType.Array_String.toString().equals(rightType)) { + addConfirmConditionTypeIssue( + object.getString("rightRef"), + operator, + consumer, + issues, + issueKeys); + } + } + for (Object child : object.values()) { + checkConfirmOutputReferences( + child, + consumer, + confirmOutputs, + issues, + issueKeys); + } + return; + } + if (value instanceof JSONArray array) { + for (Object child : array) { + checkConfirmOutputReferences( + child, + consumer, + confirmOutputs, + issues, + issueKeys); + } + } + } + + private String checkConfirmOutputReference( + String rawReference, + String rawActualType, + boolean requireActualType, + NodeView consumer, + ConfirmOutputIndex confirmOutputs, + List issues, + Set issueKeys) { + String reference = trimToNull(rawReference); + if (reference == null) { + return null; + } + String expectedType = confirmOutputs.get(reference); + if (expectedType == null + && !confirmOutputs.referencesConfirmNode(reference)) { + return null; + } + String actualType = trimToNull(rawActualType); + boolean invalidType = actualType == null + ? requireActualType + : !Objects.equals(expectedType, actualType); + if (expectedType == null || invalidType) { + addIssue( + issues, + issueKeys, + "CONFIRM_OUTPUT_REFERENCE_INVALID", + "用户确认输出引用不存在或类型已变化: " + + reference, + consumer.id, + null, + consumer.name); + } + return expectedType; + } + + private void addConfirmConditionTypeIssue( + String reference, + String operator, + NodeView consumer, + List issues, + Set issueKeys) { + addIssue( + issues, + issueKeys, + "CONFIRM_OUTPUT_REFERENCE_INVALID", + "用户确认多选输出不支持当前条件操作符: " + + trimToNull(reference) + " (" + operator + ")", + consumer.id, + null, + consumer.name); + } + /** * 将 JSON 条件规则转换为运行时规则对象。 * @@ -1674,6 +1963,35 @@ public class WorkflowCheckService { return result; } + private static class ConfirmOutputIndex { + private final Map outputTypes = new HashMap<>(); + private final Set nodeIds = new HashSet<>(); + + private void put(String nodeId, String outputName, String dataType) { + nodeIds.add(nodeId); + outputTypes.put(nodeId + "." + outputName, dataType); + } + + private String get(String reference) { + return outputTypes.get(reference); + } + + private boolean isEmpty() { + return outputTypes.isEmpty(); + } + + private boolean referencesConfirmNode(String reference) { + int separator = reference.indexOf('.'); + while (separator > 0) { + if (nodeIds.contains(reference.substring(0, separator))) { + return true; + } + separator = reference.indexOf('.', separator + 1); + } + return false; + } + } + private void throwIfFailed(WorkflowCheckResult result) { if (result == null) { return; diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowResumeService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowResumeService.java new file mode 100644 index 00000000..a5851ca5 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowResumeService.java @@ -0,0 +1,49 @@ +package tech.easyflow.ai.easyagentsflow.service; + +import com.easyagents.flow.core.chain.ChainResumeException; +import com.easyagents.flow.core.chain.runtime.ChainExecutor; +import org.springframework.stereotype.Service; +import tech.easyflow.common.web.exceptions.BusinessException; + +import javax.annotation.Resource; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 统一校验并恢复暂停中的工作流。 + */ +@Service +public class WorkflowResumeService { + + @Resource + private ChainExecutor chainExecutor; + + /** + * 恢复暂停实例,并将引擎校验失败转换为稳定的接口错误。 + * + * @param executeId 工作流实例 ID + * @param variables 用户提交的确认参数 + */ + public void resume(String executeId, Map variables) { + Map submitted = variables == null + ? new LinkedHashMap<>() + : new LinkedHashMap<>(variables); + final boolean resumed; + try { + resumed = chainExecutor.resumeAsyncIfSuspended( + executeId, submitted); + } catch (ChainResumeException exception) { + throw new BusinessException( + 400, + 40031, + exception.getMessage(), + exception); + } + if (!resumed) { + throw new BusinessException( + 409, + 40901, + "当前执行状态不可恢复,仅暂停中的工作流允许恢复"); + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowServiceTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowServiceTest.java index 0a98a4a1..dd79ac45 100644 --- a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowServiceTest.java +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowServiceTest.java @@ -6,6 +6,7 @@ import com.easyagents.flow.core.chain.ChainStatus; import com.easyagents.flow.core.chain.ExceptionSummary; import com.easyagents.flow.core.chain.NodeState; import com.easyagents.flow.core.chain.NodeStatus; +import com.easyagents.flow.core.chain.Parameter; import com.easyagents.flow.core.chain.repository.ChainStateRepository; import com.easyagents.flow.core.chain.repository.NodeStateRepository; import com.easyagents.flow.core.chain.runtime.ChainExecutor; @@ -148,6 +149,56 @@ public class TinyFlowServiceTest { .load(EXECUTE_ID, NODE_ID); } + /** + * 验证链级挂起参数只返回给真正暂停的节点。 + * + * @throws Exception 测试依赖注入失败时抛出 + */ + @Test + public void shouldAttachSuspendParametersOnlyToSuspendedNode() + throws Exception { + ChainExecutor chainExecutor = mock(ChainExecutor.class); + ChainStateRepository chainStateRepository = + mock(ChainStateRepository.class); + NodeStateRepository nodeStateRepository = + mock(NodeStateRepository.class); + ChainState chainState = new ChainState(); + chainState.setStatus(ChainStatus.SUSPEND); + Parameter parameter = new Parameter(); + parameter.setName("selection__confirm-1"); + chainState.setSuspendForParameters(List.of(parameter)); + NodeState suspendedState = new NodeState(); + suspendedState.setStatus(NodeStatus.SUSPEND); + NodeState readyState = new NodeState(); + readyState.setStatus(NodeStatus.READY); + when(chainExecutor.getChainStateRepository()) + .thenReturn(chainStateRepository); + when(chainExecutor.getNodeStateRepository()) + .thenReturn(nodeStateRepository); + when(chainStateRepository.load(EXECUTE_ID)) + .thenReturn(chainState); + when(nodeStateRepository.load(EXECUTE_ID, "confirm-1")) + .thenReturn(suspendedState); + when(nodeStateRepository.load(EXECUTE_ID, "confirm-2")) + .thenReturn(readyState); + TinyFlowService service = service(chainExecutor); + NodeInfo first = new NodeInfo(); + first.setNodeId("confirm-1"); + NodeInfo second = new NodeInfo(); + second.setNodeId("confirm-2"); + second.setSuspendForParameters(List.of(parameter)); + + ChainInfo result = service.getChainStatus( + EXECUTE_ID, List.of(first, second)); + + Assert.assertEquals( + List.of(parameter), + result.getNodes().get("confirm-1") + .getSuspendForParameters()); + Assert.assertNull(result.getNodes().get("confirm-2") + .getSuspendForParameters()); + } + /** * 验证 JavaScript 执行错误使用面向试运行用户的定位信息。 * diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckServiceTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckServiceTest.java index e903c5f0..63af966b 100644 --- a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckServiceTest.java +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckServiceTest.java @@ -525,6 +525,374 @@ public class WorkflowCheckServiceTest { assertHasCode(result, "LOOP_PARENT_CYCLE"); } + /** + * 验证用户确认选项与输出定义一致时可以保存。 + */ + @Test + public void testSaveShouldPassValidConfirmContract() throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + String content = workflowJson( + array(node( + "confirm-1", + "confirmNode", + null, + confirmData(false, "String"))), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertTrue(result.isPassed()); + } + + /** + * 验证用户确认节点拒绝重复的选项内容。 + */ + @Test + public void testSaveShouldBlockDuplicateConfirmOptionContents() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject confirm = confirmData(false, "String"); + confirm.getJSONArray("options").add("第一议题"); + String content = workflowJson( + array(node("confirm-1", "confirmNode", null, confirm)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_CONFIGURATION_INVALID"); + } + + /** + * 验证用户确认节点拒绝非字符串选项。 + */ + @Test + public void testSaveShouldBlockNonStringConfirmOption() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject confirm = confirmData(false, "String"); + confirm.getJSONArray("options").add(1); + String content = workflowJson( + array(node("confirm-1", "confirmNode", null, confirm)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_CONFIGURATION_INVALID"); + } + + /** + * 验证选择方式不能依赖 JSON 隐式类型转换。 + */ + @Test + public void testSaveShouldBlockNonBooleanConfirmMode() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject confirm = confirmData(false, "String"); + confirm.put("multiple", "false"); + String content = workflowJson( + array(node("confirm-1", "confirmNode", null, confirm)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_CONFIGURATION_INVALID"); + } + + /** + * 验证最终确认契约拒绝继续保存已废弃配置。 + */ + @Test + public void testSaveShouldBlockRetiredConfirmData() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject confirm = confirmData(false, "String"); + confirm.put("async", true); + confirm.put("fields", new JSONArray()); + confirm.put("confirms", new JSONArray()); + confirm.put("schemaVersion", 1); + confirm.put("unknownLegacySetting", true); + String content = workflowJson( + array(node("confirm-1", "confirmNode", null, confirm)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_CONFIGURATION_INVALID"); + } + + /** + * 验证多选模式必须声明数组输出。 + */ + @Test + public void testSaveShouldBlockMismatchedConfirmOutputType() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + String content = workflowJson( + array(node( + "confirm-1", + "confirmNode", + null, + confirmData(true, "String"))), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_SCHEMA_INVALID"); + } + + /** + * 验证确认节点可以使用用户配置的输出参数名称及下游引用。 + */ + @Test + public void testSaveShouldPassConfiguredConfirmOutputName() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject confirm = confirmData( + false, "String", "templateChoice"); + JSONObject consumer = data("下游节点"); + consumer.put("parameters", array(refParameter( + "input", "confirm-1.templateChoice", "String"))); + String content = workflowJson( + array( + node("confirm-1", "confirmNode", null, confirm), + node("code-1", "codeNode", null, consumer)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertTrue(result.isPassed()); + } + + /** + * 验证空输出定义不能利用空类型绕过字段契约校验。 + */ + @Test + public void testSaveShouldBlockEmptyConfirmOutputDefinition() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject confirm = confirmData(false, "String"); + confirm.put("outputDefs", array(new JSONObject())); + String content = workflowJson( + array(node("confirm-1", "confirmNode", null, confirm)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_SCHEMA_INVALID"); + } + + /** + * 验证字符串编码的输出定义不能通过 JSON 隐式类型转换。 + */ + @Test + public void testSaveShouldBlockStringEncodedConfirmOutputDefinition() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject confirm = confirmData(false, "String"); + confirm.put( + "outputDefs", + "[{\"name\":\"selection\",\"dataType\":\"String\"}]"); + String content = workflowJson( + array(node("confirm-1", "confirmNode", null, confirm)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_SCHEMA_INVALID"); + } + + /** + * 验证输出定义数组中的字符串元素不能被隐式解析为对象。 + */ + @Test + public void testSaveShouldBlockStringElementConfirmOutputDefinition() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject confirm = confirmData(false, "String"); + JSONArray outputDefs = new JSONArray(); + outputDefs.add("{\"name\":\"selection\",\"dataType\":\"String\"}"); + confirm.put("outputDefs", outputDefs); + String content = workflowJson( + array(node("confirm-1", "confirmNode", null, confirm)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_SCHEMA_INVALID"); + } + + /** + * 验证不存在的确认输出会阻止下游引用。 + */ + @Test + public void testSaveShouldBlockMissingConfirmOutputReference() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject consumer = data("下游节点"); + consumer.put("parameters", array(refParameter( + "input", "confirm-1.removed", "String"))); + String content = workflowJson( + array( + node("confirm-1", "confirmNode", null, + confirmData(false, "String")), + node("code-1", "codeNode", null, consumer)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_REFERENCE_INVALID"); + } + + /** + * 验证手工导入的带点节点 ID 也能识别失效确认输出引用。 + */ + @Test + public void testSaveShouldBlockMissingConfirmOutputForDottedNodeId() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject consumer = data("下游节点"); + consumer.put("parameters", array(refParameter( + "input", "confirm.group-1.removed", "String"))); + String content = workflowJson( + array( + node("confirm.group-1", "confirmNode", null, + confirmData(false, "String")), + node("code-1", "codeNode", null, consumer)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_REFERENCE_INVALID"); + } + + /** + * 验证切换选择方式后,保存会拦截仍声明旧类型的下游引用。 + */ + @Test + public void testSaveShouldBlockMismatchedConfirmOutputReferenceType() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject consumer = data("下游节点"); + consumer.put("parameters", array(refParameter( + "input", "confirm-1.selection", "Array"))); + String content = workflowJson( + array( + node("confirm-1", "confirmNode", null, + confirmData(false, "String")), + node("code-1", "codeNode", null, consumer)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_REFERENCE_INVALID"); + } + + /** + * 验证普通参数引用不能通过省略类型绕过确认输出契约。 + */ + @Test + public void testSaveShouldBlockConfirmOutputReferenceWithoutType() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject reference = refParameter( + "input", "confirm-1.selection", "String"); + reference.remove("dataType"); + JSONObject consumer = data("下游节点"); + consumer.put("parameters", array(reference)); + String content = workflowJson( + array( + node("confirm-1", "confirmNode", null, + confirmData(false, "String")), + node("code-1", "codeNode", null, consumer)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_REFERENCE_INVALID"); + } + + /** + * 验证条件节点直接保存的左右引用也受确认输出契约约束。 + */ + @Test + public void testSaveShouldBlockMissingConfirmOutputConditionReference() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject condition = conditionData( + "equals", "ref", "confirm-1.removed"); + JSONObject rule = condition.getJSONArray("branches") + .getJSONObject(0) + .getJSONArray("rules") + .getJSONObject(0); + rule.put("leftRef", "confirm-1.selection"); + String content = workflowJson( + array( + node("confirm-1", "confirmNode", null, + confirmData(false, "String")), + node("condition-1", "conditionNode", null, condition)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_REFERENCE_INVALID"); + } + + /** + * 验证确认字段切成多选后,不兼容的条件操作符会在保存阶段被阻止。 + */ + @Test + public void testSaveShouldBlockIncompatibleConfirmArrayConditionReference() + throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + JSONObject condition = conditionData( + "regexMatch", "fixed", "^AGENDA$"); + condition.getJSONArray("branches") + .getJSONObject(0) + .getJSONArray("rules") + .getJSONObject(0) + .put("leftRef", "confirm-1.selection"); + String content = workflowJson( + array( + node("confirm-1", "confirmNode", null, + confirmData(true, "Array")), + node("condition-1", "conditionNode", null, condition)), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONFIRM_OUTPUT_REFERENCE_INVALID"); + } + @Test public void testSaveShouldPassForValidDraft() throws Exception { WorkflowCheckService service = newService(new HashMap<>()); @@ -1011,6 +1379,14 @@ public class WorkflowCheckServiceTest { return array; } + private static JSONArray stringArray(String... values) { + JSONArray array = new JSONArray(); + for (String value : values) { + array.add(value); + } + return array; + } + private static JSONObject node(String id, String type, String parentId, JSONObject data) { JSONObject node = new JSONObject(); node.put("id", id); @@ -1080,6 +1456,25 @@ public class WorkflowCheckServiceTest { return data; } + private static JSONObject confirmData( + boolean multiple, String outputType) { + return confirmData(multiple, outputType, "selection"); + } + + private static JSONObject confirmData( + boolean multiple, String outputType, String outputName) { + JSONObject output = new JSONObject(); + output.put("name", outputName); + output.put("dataType", outputType); + + JSONObject data = data("用户确认"); + data.put("message", "请选择会议纪要模板"); + data.put("multiple", multiple); + data.put("options", stringArray("第一议题", "审议类", "听取类")); + data.put("outputDefs", array(output)); + return data; + } + /** * 创建显式循环节点数据。 * diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowResumeServiceTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowResumeServiceTest.java new file mode 100644 index 00000000..807282ae --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowResumeServiceTest.java @@ -0,0 +1,62 @@ +package tech.easyflow.ai.easyagentsflow.service; + +import com.easyagents.flow.core.chain.ChainResumeException; +import com.easyagents.flow.core.chain.runtime.ChainExecutor; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.util.Map; +import java.lang.reflect.Field; + +/** + * {@link WorkflowResumeService} 恢复错误契约测试。 + */ +public class WorkflowResumeServiceTest { + + private WorkflowResumeService service; + private ChainExecutor chainExecutor; + + @Before + public void setUp() throws Exception { + service = new WorkflowResumeService(); + chainExecutor = Mockito.mock(ChainExecutor.class); + Field field = WorkflowResumeService.class.getDeclaredField( + "chainExecutor"); + field.setAccessible(true); + field.set(service, chainExecutor); + } + + @Test + public void shouldMapInvalidConfirmationToBadRequest() { + Mockito.when(chainExecutor.resumeAsyncIfSuspended( + "execute-1", Map.of("choice", "UNKNOWN"))) + .thenThrow(new ChainResumeException("包含未配置选项")); + + try { + service.resume("execute-1", Map.of("choice", "UNKNOWN")); + Assert.fail("invalid option must be rejected"); + } catch (BusinessException exception) { + Assert.assertEquals(400, exception.getHttpStatus()); + Assert.assertEquals(40031, exception.getErrorCode()); + Assert.assertEquals("包含未配置选项", exception.getMessage()); + } + } + + @Test + public void shouldMapNonSuspendedInstanceToConflict() { + Mockito.when(chainExecutor.resumeAsyncIfSuspended( + "execute-1", Map.of("choice", "AGENDA"))) + .thenReturn(false); + + try { + service.resume("execute-1", Map.of("choice", "AGENDA")); + Assert.fail("non-suspended instance must be rejected"); + } catch (BusinessException exception) { + Assert.assertEquals(409, exception.getHttpStatus()); + Assert.assertEquals(40901, exception.getErrorCode()); + } + } +} diff --git a/easyflow-ui-admin/app/src/views/ai/plugin/PluginRunTestModal.vue b/easyflow-ui-admin/app/src/views/ai/plugin/PluginRunTestModal.vue index 781b8584..a5c26e5b 100644 --- a/easyflow-ui-admin/app/src/views/ai/plugin/PluginRunTestModal.vue +++ b/easyflow-ui-admin/app/src/views/ai/plugin/PluginRunTestModal.vue @@ -291,23 +291,29 @@ async function executePolling(nextExecuteId: string, generation: number) { } } -function resumeChain(payload: any) { +async function resumeChain( + payload: any, + onSettled: (accepted: boolean) => void, +) { if (!executeId.value) { + onSettled(false); return; } - api - .post('/api/v1/pluginItem/testResume', { + try { + const res = await 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); }); + if (res.errorCode === 0) { + startPolling(executeId.value); + onSettled(true); + return; + } + onSettled(false); + } catch (error) { + onSettled(false); + runResultResponse.value = buildErrorResult(error); + } } function showWorkflowSteps() { diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue index b8d6055c..cc9120ec 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue @@ -50,7 +50,7 @@ import nodeNames from './customNode/nodeNames'; import { createInitialWorkflowData, isWorkflowDataEmpty, - normalizeWorkflowStartNodes, + normalizeWorkflowNodes, } from '../../../../../packages/tinyflow-ui/src/utils/workflowNodeFields'; import '@tinyflow-ai/vue/dist/index.css'; @@ -444,7 +444,7 @@ async function handleSave(showMsg: boolean = false): Promise { } saveLoading.value = true; try { - const content = normalizeWorkflowStartNodes(tinyflowRef.value?.getData()); + const content = normalizeWorkflowNodes(tinyflowRef.value?.getData()); const savedContentSignature = createWorkflowContentSignature(content); const res = await api.post('/api/v1/workflow/update', { id: workflowId.value, @@ -475,11 +475,11 @@ async function getWorkflowInfo(workflowId: any, syncFlowData: boolean = true) { : {}; const serverContent = isWorkflowDataEmpty(parsedContent) ? createInitialWorkflowData() - : normalizeWorkflowStartNodes(parsedContent); + : normalizeWorkflowNodes(parsedContent); serverContentSignature = createWorkflowContentSignature(serverContent); const draft = readWorkflowDraft(workflowId, serverContent); tinyFlowData.value = draft - ? normalizeWorkflowStartNodes(draft.content as Record) + ? normalizeWorkflowNodes(draft.content as Record) : serverContent; lastObservedWorkflowContent = tinyFlowData.value; } @@ -492,7 +492,7 @@ function persistPendingWorkflowDraft() { draftWriteTimer = undefined; return; } - const content = normalizeWorkflowStartNodes(pendingDraftContent); + const content = normalizeWorkflowNodes(pendingDraftContent); lastObservedWorkflowContent = content; pendingDraftContent = null; draftWriteTimer = undefined; @@ -531,7 +531,7 @@ function flushWorkflowDraft() { function captureCurrentWorkflowDraft() { const content = tinyflowRef.value?.getData(); if (content) { - lastObservedWorkflowContent = normalizeWorkflowStartNodes(content); + lastObservedWorkflowContent = normalizeWorkflowNodes(content); pendingDraftContent = lastObservedWorkflowContent; } flushWorkflowDraft(); @@ -554,7 +554,7 @@ function reconcileWorkflowDraftAfterSave(savedContentSignature: string) { clearPendingWorkflowDraft(); return; } - const normalizedContent = normalizeWorkflowStartNodes(currentContent); + const normalizedContent = normalizeWorkflowNodes(currentContent); lastObservedWorkflowContent = normalizedContent; if ( createWorkflowContentSignature(normalizedContent) === savedContentSignature @@ -594,7 +594,7 @@ async function runCheck( stage: WorkflowCheckStage, silentPass: boolean = false, ) { - const content = normalizeWorkflowStartNodes(tinyflowRef.value?.getData()); + const content = normalizeWorkflowNodes(tinyflowRef.value?.getData()); if (!content) { ElMessage.error($t('aiWorkflow.checkContentEmpty')); return false; @@ -756,8 +756,13 @@ async function runIndependently(node: any) { singleNode.value = node; singleRunVisible.value = true; } -function resumeChain(data: any) { - workflowForm.value?.resume(data); +async function resumeChain(data: any, onSettled: (accepted: boolean) => void) { + try { + const accepted = await workflowForm.value?.resume(data); + onSettled(accepted === true); + } catch { + onSettled(false); + } } function handleChoose(nodeName: string, value: any) { if (nodeName === nodeNames.workflowNode) { diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue index c3e31171..1191ed72 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue @@ -682,7 +682,7 @@ function buildResumeRequestExample() { { executeId: '执行ID', confirmParams: { - confirm: true, + 'selection__confirm-node-1': '审议类', }, }, null, @@ -936,6 +936,10 @@ const apiDocMarkdown = computed(() => { lines.push(buildResumeRequestExample()); lines.push('```'); lines.push(``); + lines.push( + '`confirmParams` 的键请使用暂停节点 `suspendForParameters[].name` 返回的运行参数名;确认节点固定为 `selection__<节点 ID>`。单选传一个 `options[].value` 字符串,多选传由这些值组成的字符串数组;确认节点的 `label` 与 `value` 都是配置的选项内容。', + ); + lines.push(``); lines.push(`### 响应`); lines.push(``); lines.push('```json'); @@ -971,6 +975,9 @@ const apiDocMarkdown = computed(() => { lines.push( `| 400 | 40017 | 其他工作流运行参数不合法,例如文件 URL 无法识别文件名或扩展名 |`, ); + lines.push( + `| 400 | 40031 | 确认节点恢复参数缺失、类型错误或包含未配置选项 |`, + ); lines.push(`| 401 | 40101 | 缺少 ApiKey 请求头 |`); lines.push(`| 401 | 40102 / 40103 | ApiKey 无效、禁用或过期 |`); lines.push(`| 403 | 40301 / 40302 | 缺少接口权限或工作流调用权限 |`); diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/ConfirmItem.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/ConfirmItem.vue deleted file mode 100644 index d491ba7d..00000000 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/ConfirmItem.vue +++ /dev/null @@ -1,211 +0,0 @@ - - - - - diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/ConfirmItemMulti.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/ConfirmItemMulti.vue deleted file mode 100644 index 46c16d23..00000000 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/ConfirmItemMulti.vue +++ /dev/null @@ -1,216 +0,0 @@ - - - - - diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue index b3438cf8..8e737619 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue @@ -161,7 +161,7 @@ const extraFormRef = ref(); const waitingConfirmation = ref>(); const confirmValues = ref>({}); const confirmFormRef = ref(); -const confirmSubmittingAction = ref<'' | 'confirm' | 'reject'>(''); +const confirmSubmittingAction = ref<'' | 'continue'>(''); const confirmError = ref(''); const detailVisible = ref(false); const detailLoading = ref(false); @@ -252,21 +252,9 @@ const hiddenParameterSummaryCount = computed(() => ), ); const confirmParameters = computed(() => { - const parameters = Array.isArray(waitingConfirmation.value?.parameters) + return Array.isArray(waitingConfirmation.value?.parameters) ? waitingConfirmation.value?.parameters : []; - return parameters.filter( - (parameter: any) => parameter.formType !== 'confirm', - ); -}); -const confirmKey = computed(() => { - const parameters = Array.isArray(waitingConfirmation.value?.parameters) - ? waitingConfirmation.value?.parameters - : []; - return ( - parameters.find((parameter: any) => parameter.formType === 'confirm') - ?.name || '' - ); }); const composerDisabled = computed( () => @@ -1012,33 +1000,25 @@ function finalizeLiveExecutionSteps( function initializeConfirmValues(parameters: unknown) { const values: Record = {}; for (const parameter of Array.isArray(parameters) ? parameters : []) { - if (parameter.formType === 'confirm') { - continue; - } - values[parameter.name] = parameter.defaultValue ?? ''; + values[parameter.name] = + parameter.formType === 'checkbox' ? [] : (parameter.defaultValue ?? ''); } confirmValues.value = values; } -async function resumeExecution(confirmed: boolean) { - if (!executeId.value || !confirmKey.value || confirmSubmittingAction.value) { +async function resumeExecution() { + if (!executeId.value || confirmSubmittingAction.value) { return; } - confirmSubmittingAction.value = confirmed ? 'confirm' : 'reject'; + confirmSubmittingAction.value = 'continue'; confirmError.value = ''; try { - if ( - confirmed && - !(await confirmFormRef.value?.validate().catch(() => false)) - ) { + if (!(await confirmFormRef.value?.validate().catch(() => false))) { return; } await api.post(workflowChatEndpoint('resume'), { executeId: executeId.value, - confirmParams: { - [confirmKey.value]: confirmed ? 'yes' : 'no', - ...(confirmed ? confirmValues.value : {}), - }, + confirmParams: { ...confirmValues.value }, }); waitingConfirmation.value = undefined; executionState.value = 'running'; @@ -1523,20 +1503,13 @@ function executionTraceText( {{ confirmError }}

- - 拒绝 - - 确认 + 继续
diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowForm.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowForm.vue index bee8db98..9040f518 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowForm.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowForm.vue @@ -87,14 +87,22 @@ watch( }, ); const executeId = ref(''); -function resume(data: any) { +async function resume(data: any) { data.executeId = executeId.value; submitLoading.value = true; - api.post('/api/v1/workflow/resume', data).then((res) => { + let accepted = false; + try { + const res = await api.post('/api/v1/workflow/resume', data); if (res.errorCode === 0) { + accepted = true; startPolling(executeId.value); } - }); + return accepted; + } finally { + if (!accepted) { + submitLoading.value = false; + } + } } function submitV2() { runForm.value?.validate((valid) => { @@ -132,7 +140,7 @@ function startPolling(executeId: any) { if (pollingActive) return; pollingActive = true; pollingGeneration += 1; - schedulePolling(executeId, pollingGeneration); + void executePolling(executeId, pollingGeneration); } function schedulePolling(executeId: any, generation: number) { timer.value = setTimeout(() => { diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFormItem.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFormItem.vue index 5cb01b19..548a4ea9 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFormItem.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFormItem.vue @@ -59,6 +59,12 @@ function isWideItem(item: any) { return item.formType === 'textarea' || contentType === 'image'; } function getCheckboxOptions(item: any) { + if (Array.isArray(item.options)) { + return item.options.map((option: any) => ({ + label: option.label, + value: option.value, + })); + } if (item.enums) { return ( item.enums?.map((option: any) => ({ diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowSteps.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowSteps.vue index ecaf0099..8230c07e 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowSteps.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowSteps.vue @@ -14,14 +14,11 @@ import { ElCollapse, ElCollapseItem, ElForm, - ElFormItem, ElIcon, } from 'element-plus'; import ShowJson from '#/components/json/ShowJson.vue'; -import { $t } from '#/locales'; -import ConfirmItem from '#/views/ai/workflow/components/ConfirmItem.vue'; -import ConfirmItemMulti from '#/views/ai/workflow/components/ConfirmItemMulti.vue'; +import WorkflowFormItem from '#/views/ai/workflow/components/WorkflowFormItem.vue'; export interface WorkflowStepsProps { workflowId: any; @@ -31,7 +28,9 @@ export interface WorkflowStepsProps { expandAll?: boolean; } const props = defineProps(); -const emit = defineEmits(['resume']); +const emit = defineEmits<{ + resume: [payload: any, onSettled: (accepted: boolean) => void]; +}>(); const nodes = ref([]); const nodeStatusMap = ref>({}); const activeNames = ref([]); @@ -90,7 +89,7 @@ watch( } const currentNodes = newVal.nodes || {}; chainErrMsg.value = newVal.status === 21 ? newVal.message : ''; - if (![20, 21].includes(newVal.status)) { + if (Number(newVal.status) !== 5) { confirmBtnLoading.value = false; } let autoExpandNodeId: string | undefined; @@ -106,6 +105,10 @@ watch( continue; } nodeStatusMap.value[nodeId] = currentNodeState; + if (Number(currentStatus) === 5 && Number(previousStatus) !== 5) { + initializeConfirmParams(currentNodeState?.suspendForParameters); + confirmBtnLoading.value = false; + } if ( !userControlledExpansion.value && !props.expandAll && @@ -128,6 +131,7 @@ watch( () => props.initSignal, () => { nodeStatusMap.value = {}; + confirmParams.value = {}; confirmBtnLoading.value = false; chainErrMsg.value = ''; userControlledExpansion.value = false; @@ -164,8 +168,13 @@ const setFormRef = (el: any, key: string) => { formRefs.value[key] = el as FormInstance; } }; -function getSelectMode(ops: any) { - return ops.formType || 'radio'; +function initializeConfirmParams(parameters: any) { + const values: Record = {}; + for (const parameter of Array.isArray(parameters) ? parameters : []) { + values[parameter.name] = + parameter.formType === 'checkbox' ? [] : (parameter.defaultValue ?? ''); + } + confirmParams.value = values; } function handleConfirm(node: any) { const nodeKey = node.key; @@ -176,17 +185,27 @@ function handleConfirm(node: any) { console.warn(`Form instance for ${nodeKey} not found`); return; } - const confirmKey = node.suspendForParameters[0].name; form.validate((valid) => { if (valid) { const value = { - confirmParams: { - [confirmKey]: 'yes', - ...confirmParams.value, - }, + confirmParams: { ...confirmParams.value }, }; confirmBtnLoading.value = true; - emit('resume', value); + emit('resume', value, (accepted) => { + confirmBtnLoading.value = false; + if (!accepted) { + return; + } + confirmParams.value = {}; + const currentState = nodeStatusMap.value[nodeKey]; + if (currentState) { + nodeStatusMap.value[nodeKey] = { + ...currentState, + status: 1, + suspendForParameters: [], + }; + } + }); } }); } @@ -234,7 +253,12 @@ function handleConfirm(node: any) {