feat: 完善工作流 Public API 调用能力

- 支持 JSON 文件 URL 简写与 Multipart 单请求文件上传

- 完善执行拓扑、枚举状态、节点名称、恢复校验和安全错误响应

- 增加临时上传生命周期清理并升级 MinIO SDK

- 重构工作流接口调用说明弹窗的扁平响应式布局
This commit is contained in:
2026-08-09 21:27:30 +08:00
parent 0d14f1c165
commit 54d85ae460
61 changed files with 8131 additions and 161 deletions

View File

@@ -0,0 +1,100 @@
package tech.easyflow.publicapi.dto;
import java.io.Serializable;
import java.util.List;
/**
* Public API 可安全返回的机器可读错误详情。
*/
public class PublicApiErrorDetail implements Serializable {
private static final long serialVersionUID = 1L;
private final String requestId;
private final String location;
private final String field;
private final String actual;
private final List<String> expected;
private final boolean retryable;
/**
* 创建公共错误详情。
*
* @param requestId 请求关联标识
* @param location 错误位置
* @param field 错误字段
* @param actual 经脱敏和限长的实际值
* @param expected 合法值或格式
* @param retryable 是否适合直接重试
*/
public PublicApiErrorDetail(
String requestId,
String location,
String field,
String actual,
List<String> expected,
boolean retryable) {
this.requestId = requestId;
this.location = location;
this.field = field;
this.actual = actual;
this.expected = expected == null
? List.of()
: List.copyOf(expected);
this.retryable = retryable;
}
/**
* 获取请求关联标识。
*
* @return 请求关联标识
*/
public String getRequestId() {
return requestId;
}
/**
* 获取错误位置。
*
* @return 错误位置
*/
public String getLocation() {
return location;
}
/**
* 获取错误字段。
*
* @return 错误字段
*/
public String getField() {
return field;
}
/**
* 获取安全实际值。
*
* @return 实际值
*/
public String getActual() {
return actual;
}
/**
* 获取期望值。
*
* @return 不可变期望值列表
*/
public List<String> getExpected() {
return expected;
}
/**
* 判断是否适合直接重试。
*
* @return 是否可重试
*/
public boolean isRetryable() {
return retryable;
}
}

View File

@@ -0,0 +1,37 @@
package tech.easyflow.publicapi.dto;
import java.io.Serializable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 工作流 Public API 执行状态。
*
* @param executeId 执行 ID
* @param status 可读工作流状态
* @param terminal 是否已经进入终态
* @param message 安全错误消息
* @param result 工作流执行结果
* @param nodes 节点 ID 到节点状态的映射
* @param error 安全错误对象
*/
public record PublicWorkflowChainStatus(
String executeId,
PublicWorkflowExecutionStatus status,
boolean terminal,
String message,
Map<String, Object> result,
Map<String, PublicWorkflowNodeStatus> nodes,
PublicWorkflowStatusError error) implements Serializable {
/**
* 创建不可变公共执行状态。
*/
public PublicWorkflowChainStatus {
nodes = nodes == null
? Map.of()
: Collections.unmodifiableMap(
new LinkedHashMap<>(nodes));
}
}

View File

@@ -0,0 +1,116 @@
package tech.easyflow.publicapi.dto;
import com.easyagents.flow.core.chain.ChainStatus;
import com.easyagents.flow.core.chain.NodeStatus;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* 工作流 Public API 的可读执行状态。
*/
public enum PublicWorkflowExecutionStatus {
/** 尚未开始。 */
READY("ready", false),
/** 正在执行。 */
RUNNING("running", false),
/** 等待外部参数恢复。 */
SUSPENDED("suspended", false),
/** 执行发生暂态错误,运行时仍可能继续处理。 */
ERROR("error", false),
/** 已成功完成。 */
DONE("done", true),
/** 已失败结束。 */
FAILED("failed", true),
/** 已取消。 */
CANCELLED("cancelled", true),
/** 无法识别的状态。 */
UNKNOWN("unknown", false);
/** 对外字符串值。 */
private final String value;
/** 是否为终态。 */
private final boolean terminal;
/**
* 创建公开执行状态。
*
* @param value 对外字符串值
* @param terminal 是否为终态
*/
PublicWorkflowExecutionStatus(
String value,
boolean terminal) {
this.value = value;
this.terminal = terminal;
}
/**
* 获取 JSON 响应中的状态值。
*
* @return 小写状态值
*/
@JsonValue
public String getValue() {
return value;
}
/**
* 判断是否为终态。
*
* @return 已结束时返回 {@code true}
*/
public boolean isTerminal() {
return terminal;
}
/**
* 将内部工作流状态转换为公开枚举。
*
* @param status 内部数值状态
* @return 公开状态枚举
*/
public static PublicWorkflowExecutionStatus fromChainStatus(
Integer status) {
if (status == null) {
return UNKNOWN;
}
ChainStatus chainStatus = ChainStatus.fromValue(status);
if (chainStatus == null) {
return UNKNOWN;
}
return switch (chainStatus) {
case READY -> READY;
case RUNNING -> RUNNING;
case SUSPEND -> SUSPENDED;
case ERROR -> ERROR;
case SUCCEEDED -> DONE;
case FAILED -> FAILED;
case CANCELLED -> CANCELLED;
};
}
/**
* 将内部节点状态转换为公开枚举。
*
* @param status 内部数值状态
* @return 公开状态枚举
*/
public static PublicWorkflowExecutionStatus fromNodeStatus(
Integer status) {
if (status == null) {
return UNKNOWN;
}
NodeStatus nodeStatus = NodeStatus.fromValue(status);
if (nodeStatus == null) {
return UNKNOWN;
}
return switch (nodeStatus) {
case READY -> READY;
case RUNNING -> RUNNING;
case SUSPEND -> SUSPENDED;
case ERROR -> ERROR;
case SUCCEEDED -> DONE;
case FAILED -> FAILED;
};
}
}

View File

@@ -0,0 +1,53 @@
package tech.easyflow.publicapi.dto;
import tech.easyflow.ai.entity.Workflow;
import java.io.Serializable;
/**
* 工作流 Public API 的安全基础信息。
*
* @param id 工作流 ID
* @param alias 工作流别名
* @param title 工作流标题
* @param description 工作流描述
* @param icon 工作流图标
* @param revision 当前发布修订号
* @param publishedAt 发布时间ISO-8601 格式
*/
public record PublicWorkflowInfo(
String id,
String alias,
String title,
String description,
String icon,
Integer revision,
String publishedAt) implements Serializable {
/**
* 从已发布工作流视图创建安全基础信息。
*
* @param workflow 已发布工作流视图
* @return 安全基础信息
* @throws IllegalArgumentException 工作流为空时抛出
*/
public static PublicWorkflowInfo from(Workflow workflow) {
if (workflow == null) {
throw new IllegalArgumentException("workflow must not be null");
}
return new PublicWorkflowInfo(
workflow.getId() == null
? null
: workflow.getId().toString(),
workflow.getAlias(),
workflow.getTitle(),
workflow.getDescription(),
workflow.getIcon(),
workflow.getRevision(),
workflow.getPublishedAt() == null
? null
: workflow.getPublishedAt()
.toInstant()
.toString());
}
}

View File

@@ -0,0 +1,26 @@
package tech.easyflow.publicapi.dto;
import com.easyagents.flow.core.chain.Parameter;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* 工作流 Public API 的节点执行状态。
*
* @param nodeId 节点 ID
* @param nodeName 节点名称
* @param status 可读节点状态
* @param message 安全错误消息
* @param result 节点执行结果
* @param suspendForParameters 暂停时等待补充的参数
*/
public record PublicWorkflowNodeStatus(
String nodeId,
String nodeName,
PublicWorkflowExecutionStatus status,
String message,
Map<String, Object> result,
List<Parameter> suspendForParameters) implements Serializable {
}

View File

@@ -0,0 +1,55 @@
package tech.easyflow.publicapi.dto;
import jakarta.validation.constraints.NotNull;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Public Workflow API JSON 与 Multipart 执行元数据。
*/
public class PublicWorkflowRunMetadata {
@NotNull(message = "metadata.id 不能为空")
private BigInteger id;
private Map<String, Object> variables = new LinkedHashMap<>();
/**
* 获取工作流 ID。
*
* @return 工作流 ID
*/
public BigInteger getId() {
return id;
}
/**
* 设置工作流 ID。
*
* @param id 工作流 ID
*/
public void setId(BigInteger id) {
this.id = id;
}
/**
* 获取普通运行变量。
*
* @return 普通运行变量
*/
public Map<String, Object> getVariables() {
return variables;
}
/**
* 设置普通运行变量。
*
* @param variables 普通运行变量
*/
public void setVariables(Map<String, Object> variables) {
this.variables = variables == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(variables);
}
}

View File

@@ -0,0 +1,53 @@
package tech.easyflow.publicapi.dto;
import tech.easyflow.common.constant.enums.EnumRes;
import tech.easyflow.common.domain.Result;
/**
* Public Workflow API 执行响应。
*
* <p>继承原有 {@link Result} 并继续把执行 ID 放在 {@code data}
* 新增 {@code workflow} 区块以保持旧调用方兼容。</p>
*/
public class PublicWorkflowRunResult extends Result<String> {
private static final long serialVersionUID = 1L;
private PublicWorkflowTopology workflow;
/**
* 创建成功响应。
*
* @param executeId 工作流执行 ID
* @param workflow 已发布工作流拓扑
* @return 成功响应
*/
public static PublicWorkflowRunResult success(
String executeId,
PublicWorkflowTopology workflow) {
PublicWorkflowRunResult result = new PublicWorkflowRunResult();
result.setErrorCode(EnumRes.SUCCESS.getCode());
result.setMessage(EnumRes.SUCCESS.getMsg());
result.setData(executeId);
result.setWorkflow(workflow);
return result;
}
/**
* 获取已发布工作流拓扑。
*
* @return 工作流拓扑
*/
public PublicWorkflowTopology getWorkflow() {
return workflow;
}
/**
* 设置已发布工作流拓扑。
*
* @param workflow 工作流拓扑
*/
public void setWorkflow(PublicWorkflowTopology workflow) {
this.workflow = workflow;
}
}

View File

@@ -0,0 +1,84 @@
package tech.easyflow.publicapi.dto;
import java.io.Serializable;
/**
* 工作流公共执行状态中的安全错误信息。
*/
public class PublicWorkflowStatusError implements Serializable {
private static final long serialVersionUID = 1L;
private final String code;
private final String message;
private final String nodeId;
private final String nodeName;
private final boolean retryable;
/**
* 创建安全执行错误。
*
* @param code 稳定错误标识
* @param message 安全错误消息
* @param nodeId 失败节点 ID
* @param nodeName 失败节点名称
* @param retryable 当前状态是否仍可能恢复
*/
public PublicWorkflowStatusError(
String code,
String message,
String nodeId,
String nodeName,
boolean retryable) {
this.code = code;
this.message = message;
this.nodeId = nodeId;
this.nodeName = nodeName;
this.retryable = retryable;
}
/**
* 获取错误标识。
*
* @return 错误标识
*/
public String getCode() {
return code;
}
/**
* 获取安全消息。
*
* @return 安全消息
*/
public String getMessage() {
return message;
}
/**
* 获取失败节点 ID。
*
* @return 节点 ID
*/
public String getNodeId() {
return nodeId;
}
/**
* 获取失败节点名称。
*
* @return 节点名称
*/
public String getNodeName() {
return nodeName;
}
/**
* 判断当前状态是否仍可能恢复。
*
* @return 是否可重试
*/
public boolean isRetryable() {
return retryable;
}
}

View File

@@ -0,0 +1,166 @@
package tech.easyflow.publicapi.dto;
import java.io.Serializable;
import java.util.List;
/**
* Public Workflow API 对外公开的安全拓扑视图。
*
* @param workflowId 工作流 ID
* @param alias 工作流别名
* @param title 工作流标题
* @param description 工作流描述
* @param revision 发布内容修订号
* @param publishedAt 发布时间ISO-8601 格式
* @param nodes 按稳定拓扑顺序排列的节点
* @param edges 按发布定义顺序排列的边
* @param topologicalOrder 节点 ID 的稳定拓扑顺序
* @param topologyLevels 考虑循环体完成屏障后的可并行拓扑层级
* @param hasCycle 发布图中是否存在环
* @param unresolvedNodeIds 受环路影响而无法进入标准拓扑序的节点 ID
*/
public record PublicWorkflowTopology(
String workflowId,
String alias,
String title,
String description,
Integer revision,
String publishedAt,
List<Node> nodes,
List<Edge> edges,
List<String> topologicalOrder,
List<List<String>> topologyLevels,
boolean hasCycle,
List<String> unresolvedNodeIds) implements Serializable {
/**
* 创建不可变工作流拓扑。
*/
public PublicWorkflowTopology {
nodes = immutable(nodes);
edges = immutable(edges);
topologicalOrder = immutable(topologicalOrder);
topologyLevels = topologyLevels == null
? List.of()
: topologyLevels.stream()
.map(PublicWorkflowTopology::immutable)
.toList();
unresolvedNodeIds = immutable(unresolvedNodeIds);
}
/**
* 工作流公开节点。
*
* @param nodeId 节点 ID
* @param nodeType 节点类型
* @param nodeName 节点名称
* @param description 节点描述
* @param parentNodeId 父级容器节点 ID
* @param definitionIndex 节点在发布定义中的位置
* @param topologyIndex 节点在稳定拓扑序中的位置
* @param topologyLevel 节点所在拓扑层级
* @param inDegree 发布定义中的原始入度
* @param outDegree 发布定义中的原始出度
* @param startNode 是否开始节点
* @param endNode 是否结束节点
* @param predecessorNodeIds 直接前驱节点 ID
* @param successorNodeIds 直接后继节点 ID
* @param incomingEdgeIds 入边 ID
* @param outgoingEdgeIds 出边 ID
* @param inputParameters 节点输入参数元数据
* @param outputParameters 节点输出参数元数据
*/
public record Node(
String nodeId,
String nodeType,
String nodeName,
String description,
String parentNodeId,
int definitionIndex,
int topologyIndex,
int topologyLevel,
int inDegree,
int outDegree,
boolean startNode,
boolean endNode,
List<String> predecessorNodeIds,
List<String> successorNodeIds,
List<String> incomingEdgeIds,
List<String> outgoingEdgeIds,
List<Parameter> inputParameters,
List<Parameter> outputParameters) implements Serializable {
/**
* 创建不可变公开节点。
*/
public Node {
predecessorNodeIds = immutable(predecessorNodeIds);
successorNodeIds = immutable(successorNodeIds);
incomingEdgeIds = immutable(incomingEdgeIds);
outgoingEdgeIds = immutable(outgoingEdgeIds);
inputParameters = immutable(inputParameters);
outputParameters = immutable(outputParameters);
}
}
/**
* 工作流公开边。
*
* @param edgeId 边 ID
* @param edgeType 边类型
* @param label 边展示名称
* @param sourceNodeId 源节点 ID
* @param targetNodeId 目标节点 ID
* @param sourceHandle 源连接点
* @param targetHandle 目标连接点
* @param parentNodeId 所属父级容器节点 ID
* @param definitionIndex 边在发布定义中的位置
* @param dangling 是否引用了不存在的节点
*/
public record Edge(
String edgeId,
String edgeType,
String label,
String sourceNodeId,
String targetNodeId,
String sourceHandle,
String targetHandle,
String parentNodeId,
int definitionIndex,
boolean dangling) implements Serializable {
}
/**
* 节点输入或输出参数的安全元数据。
*
* @param parameterId 参数 ID
* @param name 参数名
* @param label 展示名称
* @param dataType 数据类型
* @param contentType 内容类型
* @param required 是否必填
* @param description 参数说明
* @param multipartPartName 开始节点文件参数对应的 multipart Part 名
*/
public record Parameter(
String parameterId,
String name,
String label,
String dataType,
String contentType,
boolean required,
String description,
String multipartPartName) implements Serializable {
}
/**
* 将列表转换为不可变副本。
*
* @param source 原列表
* @param <T> 元素类型
* @return 不可变列表
*/
private static <T> List<T> immutable(List<T> source) {
return source == null ? List.of() : List.copyOf(source);
}
}