|
@@ -0,0 +1,423 @@
|
|
|
|
|
+# 工作流节点输出统一包装(`{status, message, data}`)实施方案 v2
|
|
|
|
|
+
|
|
|
|
|
+> 修订说明:本版在 v1(机械包装)基础上新增"灵活产出 + 重试机制"。已确认设计决策:
|
|
|
|
|
+> - **严格按画布声明**:data 内字段 = 节点 outputs/variables 声明,多余字段丢弃
|
|
|
|
|
+> - **重试触发**:仅结构问题(JSON 解析失败/必填字段缺失)+ 类型问题(无法转换)
|
|
|
|
|
+> - **SmartAction 与 LLM 节点统一逻辑**:用户在画布声明多个变量,LLM 按描述填充
|
|
|
|
|
+> - **配置粒度**:全局默认 + 节点级覆盖
|
|
|
|
|
+
|
|
|
|
|
+## 一、需求总览
|
|
|
|
|
+
|
|
|
|
|
+### 1.1 统一输出格式
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "status": 200,
|
|
|
|
|
+ "message": "调用成功",
|
|
|
|
|
+ "data": {
|
|
|
|
|
+ "fieldA": "XXXX", //(字段A输出结果)
|
|
|
|
|
+ "fieldB": 15.83 //(字段B输出结果)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- `status`:200 成功 / 400 失败
|
|
|
|
|
+- `message`:人类可读结果/失败原因
|
|
|
|
|
+- `data`:节点声明的输出变量集合(严格按画布声明,不收纳多余字段)
|
|
|
|
|
+
|
|
|
|
|
+### 1.2 后继节点解析规则
|
|
|
|
|
+
|
|
|
|
|
+- 从前序节点 **`data` 字段内部**取变量(不再读扁平 Map 顶层)
|
|
|
|
|
+- 支持读取 `data` 内的子字段(如 `data.profile.name`)
|
|
|
|
|
+- 失败节点(`status=400`)写入上下文,下游可感知失败状态
|
|
|
|
|
+
|
|
|
|
|
+### 1.3 灵活产出能力(v2 新增)
|
|
|
|
|
+
|
|
|
|
|
+| 能力 | 适用节点 | 说明 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| **结构化输出** | LLM / SmartAction / Agent / Hermes* | 即使只声明 1 个字段也走 JSON 模式,让 LLM 按字段定义产出 |
|
|
|
|
|
+| **自动重试** | 同上 | JSON 解析失败、必填字段缺失、类型转换失败时自动重试 |
|
|
|
|
|
+| **字段填充校验** | 同上 | 必填字段缺失则触发重试或失败 |
|
|
|
|
|
+
|
|
|
|
|
+### 1.4 SmartAction 升级
|
|
|
|
|
+
|
|
|
|
|
+- 画布上支持声明 outputs 列表(与 LLM 节点一致)
|
|
|
|
|
+- 执行器读取 `actionPrompt`(操作要求)作为 user prompt
|
|
|
|
|
+- 走与 LLM 节点完全一致的结构化输出 + 重试流程
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 二、核心设计
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 `NodeOutputEnvelope` 值对象
|
|
|
|
|
+
|
|
|
|
|
+**文件**:`backend/.../engine/NodeOutputEnvelope.java`
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public class NodeOutputEnvelope {
|
|
|
|
|
+ public static final int STATUS_SUCCESS = 200;
|
|
|
|
|
+ public static final int STATUS_FAILED = 400;
|
|
|
|
|
+
|
|
|
|
|
+ private final int status;
|
|
|
|
|
+ private final String message;
|
|
|
|
|
+ private final Map<String, Object> data;
|
|
|
|
|
+
|
|
|
|
|
+ public static NodeOutputEnvelope success(String message, Map<String, Object> data);
|
|
|
|
|
+ public static NodeOutputEnvelope failure(String message);
|
|
|
|
|
+ public static NodeOutputEnvelope of(int status, String message, Map<String, Object> data);
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> toMap();
|
|
|
|
|
+ public static NodeOutputEnvelope fromObject(Object raw);
|
|
|
|
|
+ public static boolean isEnvelope(Object raw);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2.2 `StructuredOutputHelper` 增强(核心)
|
|
|
|
|
+
|
|
|
|
|
+**文件**:`backend/.../engine/StructuredOutputHelper.java`
|
|
|
|
|
+
|
|
|
|
|
+**改造点**:
|
|
|
|
|
+
|
|
|
|
|
+1. **强制 JSON 输出**:`needsStructuredOutput()` 改为只要 `outputs` 非空就返回 true(移除 `size > 1` 判断),让所有 LLM 类节点统一走结构化模式。
|
|
|
|
|
+
|
|
|
|
|
+2. **新增 `extractWithRetry()` 方法**:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public static class ExtractionResult {
|
|
|
|
|
+ public final Map<String, Object> data;
|
|
|
|
|
+ public final String errorMessage; // null 表示成功
|
|
|
|
|
+ public final int attempts; // 实际尝试次数
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+public static ExtractionResult extractWithRetry(
|
|
|
|
|
+ ChatClient client,
|
|
|
|
|
+ String userPrompt,
|
|
|
|
|
+ String systemPrompt,
|
|
|
|
|
+ JsonNode outputsDecl,
|
|
|
|
|
+ RetryPolicy retryPolicy
|
|
|
|
|
+);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**流程**:
|
|
|
|
|
+```
|
|
|
|
|
+for (attempt = 1; attempt <= maxRetries; attempt++) {
|
|
|
|
|
+ String prompt = userPrompt + buildInstruction(outputsDecl, lastError);
|
|
|
|
|
+ String response = client.prompt().user(prompt).system(systemPrompt).call().content();
|
|
|
|
|
+ Map<String, Object> parsed = parse(response, outputsDecl);
|
|
|
|
|
+ ValidationResult validation = OutputValidator.validate(parsed, outputsDecl);
|
|
|
|
|
+ if (validation.ok) return ExtractionResult.success(parsed, attempt);
|
|
|
|
|
+ lastError = validation.errorMessage;
|
|
|
|
|
+}
|
|
|
|
|
+return ExtractionResult.failure(lastError, attempt);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+3. **`buildInstruction()` 增强**:接受 `lastError` 参数,重试时在指令中追加"上一次返回错误:xxx,请修正"。
|
|
|
|
|
+
|
|
|
|
|
+4. **指令强化**:在 JSON 模板前增加"严格按要求输出,字段缺失或类型不符将导致流程失败"的强调。
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 新增 `OutputValidator`
|
|
|
|
|
+
|
|
|
|
|
+**文件**:`backend/.../engine/OutputValidator.java`
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public class OutputValidator {
|
|
|
|
|
+ public static class ValidationResult {
|
|
|
|
|
+ public final boolean ok;
|
|
|
|
|
+ public final String errorMessage;
|
|
|
|
|
+ public final List<String> missingRequired; // 缺失的必填字段
|
|
|
|
|
+ public final List<String> typeMismatched; // 类型不匹配的字段
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static ValidationResult validate(Map<String, Object> parsed, JsonNode outputsDecl);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**校验规则**:
|
|
|
|
|
+- **必填字段**:`outputs[i].required == true` 但 `parsed` 中无值或为 null → 加入 `missingRequired`
|
|
|
|
|
+- **类型校验**:声明为 `number` 但值为字符串且无法转换、声明为 `array` 但非数组、声明为 `object` 但非 Map → 加入 `typeMismatched`
|
|
|
|
|
+- **类型自动转换**:声明 `number` + 字符串数字 → 静默转换(不算 mismatch)
|
|
|
|
|
+
|
|
|
|
|
+### 2.4 新增 `RetryPolicy` (默认重试次数在application.yml中设置,同步更新application.yml.example)
|
|
|
|
|
+
|
|
|
|
|
+**文件**:`backend/.../engine/RetryPolicy.java`
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public class RetryPolicy {
|
|
|
|
|
+ public final int maxRetries; // 默认 2,节点级可覆盖
|
|
|
|
|
+ public final boolean retryOnParseError; // JSON 解析失败重试,默认 true
|
|
|
|
|
+ public final boolean retryOnMissingRequired; // 必填字段缺失重试,默认 true
|
|
|
|
|
+ public final boolean retryOnTypeMismatch; // 类型不匹配重试,默认 true
|
|
|
|
|
+
|
|
|
|
|
+ public static RetryPolicy fromNodeData(JsonNode data, WorkflowProperties props);
|
|
|
|
|
+ public static RetryPolicy defaultPolicy();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**配置读取优先级**:
|
|
|
|
|
+1. 节点 `data.maxRetries` / `data.retryOn*`
|
|
|
|
|
+2. 全局 `application.yml` 中 `workflow.llm.retry.*`
|
|
|
|
|
+3. 代码内默认值(maxRetries=2,全部 retryOn=true)
|
|
|
|
|
+
|
|
|
|
|
+### 2.5 全局配置(`application.yml`)
|
|
|
|
|
+
|
|
|
|
|
+```yaml
|
|
|
|
|
+workflow:
|
|
|
|
|
+ llm:
|
|
|
|
|
+ retry:
|
|
|
|
|
+ max-retries: 2
|
|
|
|
|
+ retry-on-parse-error: true
|
|
|
|
|
+ retry-on-missing-required: true
|
|
|
|
|
+ retry-on-type-mismatch: true
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**新增 `WorkflowProperties`** 配置类(`@ConfigurationProperties("workflow")`),通过 Spring 注入。
|
|
|
|
|
+
|
|
|
|
|
+### 2.6 各节点改造明细
|
|
|
|
|
+
|
|
|
|
|
+#### 2.6.1 LLM 类节点(LLM / SmartAction / Agent / HermesAgent / HermesSmartAction)
|
|
|
|
|
+
|
|
|
|
|
+**统一调用模式**:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+RetryPolicy policy = RetryPolicy.fromNodeData(data, workflowProperties);
|
|
|
|
|
+StructuredOutputHelper.ExtractionResult extResult = StructuredOutputHelper.extractWithRetry(
|
|
|
|
|
+ client, userPrompt, systemPrompt, outputsDecl, policy);
|
|
|
|
|
+
|
|
|
|
|
+if (extResult.errorMessage != null) {
|
|
|
|
|
+ return NodeExecutionResult.success(nodeId,
|
|
|
|
|
+ NodeOutputEnvelope.failure(extResult.errorMessage).toMap());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+Map<String, Object> data = new LinkedHashMap<>(extResult.data);
|
|
|
|
|
+data.put("elapsed_time", elapsed); // LLM 节点特有
|
|
|
|
|
+String msg = "LLM 调用成功" + (extResult.attempts > 1 ? "(重试 " + (extResult.attempts - 1) + " 次)" : "");
|
|
|
|
|
+return NodeExecutionResult.success(nodeId,
|
|
|
|
|
+ NodeOutputEnvelope.success(msg, data).toMap());
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 2.6.2 SmartActionExecutor 升级
|
|
|
|
|
+
|
|
|
|
|
+**关键改动**:支持 outputs 声明,走与 LLM 一致的结构化输出。
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@Override
|
|
|
|
|
+public NodeExecutionResult execute(...) {
|
|
|
|
|
+ String actionPrompt = TemplateRenderer.render(data.path("actionPrompt").asText(""), workspace.getVariables());
|
|
|
|
|
+ if (actionPrompt.isEmpty()) {
|
|
|
|
|
+ return NodeExecutionResult.success(nodeId,
|
|
|
|
|
+ NodeOutputEnvelope.failure("智能操作节点的操作要求为空").toMap());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JsonNode outputsDecl = data.path("outputs");
|
|
|
|
|
+ if (outputsDecl.isArray() && outputsDecl.size() > 0) {
|
|
|
|
|
+ // 结构化模式:走 LLM 节点的统一流程
|
|
|
|
|
+ return executeStructured(nodeId, data, actionPrompt, workspace, outputsDecl);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 降级模式:无 outputs 声明,按单变量透传(兼容旧工作流)
|
|
|
|
|
+ String result = callLlm(...);
|
|
|
|
|
+ String varName = NodeTypeUtils.resolveOutputVarName(data, "result");
|
|
|
|
|
+ return NodeExecutionResult.success(nodeId,
|
|
|
|
|
+ NodeOutputEnvelope.success("智能操作执行成功", Map.of(varName, result)).toMap());
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 2.6.3 KnowledgeRetrievalExecutor
|
|
|
|
|
+
|
|
|
|
|
+固定 4 字段,无需 LLM 重试。直接组装 envelope:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+return NodeExecutionResult.success(nodeId,
|
|
|
|
|
+ NodeOutputEnvelope.success("知识检索完成", output).toMap());
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 2.6.4 UserInputExecutor
|
|
|
|
|
+
|
|
|
|
|
+按 variables 取值后包装:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+return NodeExecutionResult.success(nodeId,
|
|
|
|
|
+ NodeOutputEnvelope.success("用户输入已收集", collected).toMap());
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 2.6.5 ConditionExecutor
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+Map<String, Object> data = Map.of("selectedBranch", sourceHandle);
|
|
|
|
|
+return NodeExecutionResult.success(nodeId,
|
|
|
|
|
+ NodeOutputEnvelope.success("条件路由完成:" + sourceHandle, data).toMap(), sourceHandle);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 2.6.6 OutputExecutor
|
|
|
|
|
+
|
|
|
|
|
+合并所有前置 envelope.data:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+Map<String, Object> aggregatedData = new LinkedHashMap<>();
|
|
|
|
|
+int worstStatus = STATUS_SUCCESS;
|
|
|
|
|
+List<String> messages = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+for (entry : context.getAllNodeScopedOutputs()) {
|
|
|
|
|
+ NodeOutputEnvelope env = NodeOutputEnvelope.fromObject(entry.getValue());
|
|
|
|
|
+ if (env.getStatus() == STATUS_FAILED) {
|
|
|
|
|
+ worstStatus = STATUS_FAILED;
|
|
|
|
|
+ messages.add("[" + entry.getKey() + "] " + env.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (env.getData() != null) aggregatedData.putAll(env.getData());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 工作目录文件
|
|
|
|
|
+aggregatedData.put("_workingDirFiles", files);
|
|
|
|
|
+aggregatedData.put("_runId", context.getRunId());
|
|
|
|
|
+
|
|
|
|
|
+String message = worstStatus == STATUS_SUCCESS
|
|
|
|
|
+ ? "工作流执行成功"
|
|
|
|
|
+ : "部分节点失败:" + String.join("; ", messages);
|
|
|
|
|
+return NodeExecutionResult.success(nodeId,
|
|
|
|
|
+ NodeOutputEnvelope.of(worstStatus, message, aggregatedData).toMap());
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2.7 `WorkflowContext` 改造
|
|
|
|
|
+
|
|
|
|
|
+`nodeScopedOutputs: Map<nodeId, Map<String, Object>>` 的 value 改为 envelope.toMap()。
|
|
|
|
|
+
|
|
|
|
|
+`snapshotAllOutputs()` 改为解包 envelope.data 后扁平化(保持 key 形如 `{nodeId}__{varName}`,value 是业务字段值)。
|
|
|
|
|
+
|
|
|
|
|
+### 2.8 `NodeWorkspaceBuilder` 改造
|
|
|
|
|
+
|
|
|
|
|
+`scopedOutputs` 保留 envelope 原貌;`variables` 扁平表注入解包后的 data 字段。
|
|
|
|
|
+
|
|
|
|
|
+### 2.9 `NodeInputResolver` 改造
|
|
|
|
|
+
|
|
|
|
|
+五级匹配全部穿透 envelope.data:
|
|
|
|
|
+- `workspace.getScopedOutput(nodeId, fieldName)` 先解包 envelope
|
|
|
|
|
+- 模糊匹配遍历 envelope.data.entrySet()
|
|
|
|
|
+- JsonPath 提取以 envelope.data 为根
|
|
|
|
|
+
|
|
|
|
|
+### 2.10 `WorkflowLevelExecutor` 失败处理
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+NodeExecutionResult result = executor.execute(...);
|
|
|
|
|
+Map<String, Object> output = result.getOutput();
|
|
|
|
|
+if (output != null) {
|
|
|
|
|
+ context.setNodeOutput(nodeId, output);
|
|
|
|
|
+} else if (result.getStatus() == FAILED) {
|
|
|
|
|
+ Map<String, Object> failEnv = NodeOutputEnvelope.failure(result.getError()).toMap();
|
|
|
|
|
+ context.setNodeOutput(nodeId, failEnv);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+保留 failStrategy=abort/skip 行为。
|
|
|
|
|
+
|
|
|
|
|
+### 2.11 前端适配
|
|
|
|
|
+
|
|
|
|
|
+#### `useWorkflowRunner.js` 新增 helper
|
|
|
|
|
+```js
|
|
|
|
|
+function isEnvelope(o) { return o && typeof o === 'object' && 'status' in o && 'data' in o }
|
|
|
|
|
+function unwrapNodeOutput(o) { return isEnvelope(o) ? (o.data || {}) : o }
|
|
|
|
|
+function nodeOutputStatus(o) { return isEnvelope(o) ? o.status : null }
|
|
|
|
|
+function nodeOutputMessage(o) { return isEnvelope(o) ? o.message : '' }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### SmartAction 节点配置面板(`WorkflowEditor.vue`)
|
|
|
|
|
+- 添加 outputs 列表编辑器(与 LLM 节点结构一致)
|
|
|
|
|
+- 添加"重试配置"折叠面板(maxRetries 数字输入、retryOn 复选框组)
|
|
|
|
|
+
|
|
|
|
|
+#### LLM 节点配置面板
|
|
|
|
|
+- 添加"重试配置"折叠面板(同上)
|
|
|
|
|
+
|
|
|
|
|
+#### 运行结果展示
|
|
|
|
|
+- `v-for` 遍历 `unwrapNodeOutput(r.output)` 而非 `r.output`
|
|
|
|
|
+- 失败(status=400)时显示红色徽章 + message
|
|
|
|
|
+- 重试次数 > 0 时在 message 末尾显示"(重试 N 次)"
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 三、实施阶段
|
|
|
|
|
+
|
|
|
|
|
+### 阶段 1:核心抽象(无破坏性)
|
|
|
|
|
+1. 新建 `NodeOutputEnvelope` 类
|
|
|
|
|
+2. 新建 `NodeOutputEnvelopeTest` 单元测试
|
|
|
|
|
+3. 验证:`mvn compile` + 测试通过
|
|
|
|
|
+
|
|
|
|
|
+### 阶段 2:质保组件(独立工具类)
|
|
|
|
|
+4. 新建 `OutputValidator`
|
|
|
|
|
+5. 新建 `RetryPolicy`(含 fromNodeData 解析)
|
|
|
|
|
+6. 新建 `WorkflowProperties` 配置类
|
|
|
|
|
+7. 改造 `StructuredOutputHelper`:强制 JSON 输出 + `extractWithRetry` + lastError 注入
|
|
|
|
|
+8. 新增 `StructuredOutputHelperTest`(覆盖重试、降级、类型转换)
|
|
|
|
|
+9. 验证:`mvn compile` + 单元测试
|
|
|
|
|
+
|
|
|
|
|
+### 阶段 3:LLM 类执行器改造
|
|
|
|
|
+10. `LlmExecutor`:使用 `extractWithRetry` + envelope 包装
|
|
|
|
|
+11. `SmartActionExecutor`:新增 outputs 声明支持,复用 `extractWithRetry`
|
|
|
|
|
+12. `AgentExecutor`:按 Skill outputs 走结构化(若 skill 已声明 outputs)
|
|
|
|
|
+13. `HermesAgentExecutor` / `HermesSmartActionExecutor`:同步改造
|
|
|
|
|
+14. 验证:`mvn compile`
|
|
|
|
|
+
|
|
|
|
|
+### 阶段 4:非 LLM 节点改造
|
|
|
|
|
+15. `KnowledgeRetrievalExecutor`:envelope 包装(固定 4 字段)
|
|
|
|
|
+16. `UserInputExecutor`:envelope 包装
|
|
|
|
|
+17. `ConditionExecutor`:envelope 包装 + selectedBranch 放入 data
|
|
|
|
|
+18. `OutputExecutor`:聚合所有前置 envelope.data
|
|
|
|
|
+19. 验证:`mvn compile`
|
|
|
|
|
+
|
|
|
|
|
+### 阶段 5:消费端适配
|
|
|
|
|
+20. `WorkflowContext.snapshotAllOutputs`:解包 envelope.data 扁平化
|
|
|
|
|
+21. `NodeWorkspace.getScopedOutput`:穿透 envelope
|
|
|
|
|
+22. `NodeWorkspaceBuilder.build`:注入 variables 时解包
|
|
|
|
|
+23. `NodeInputResolver`:五级匹配穿透 data
|
|
|
|
|
+24. `WorkflowLevelExecutor.executeOneNode`:失败兜底为 envelope
|
|
|
|
|
+25. 验证:`mvn compile` + NodeInputResolverTest 调整
|
|
|
|
|
+
|
|
|
|
|
+### 阶段 6:前端适配
|
|
|
|
|
+26. `useWorkflowRunner.js`:新增 unwrap helper
|
|
|
|
|
+27. `WorkflowEditor.vue` 运行结果 Tab:适配 envelope + status 徽章 + 重试次数展示
|
|
|
|
|
+28. `WorkflowEditor.vue` SmartAction 配置面板:新增 outputs 编辑器 + 重试配置面板
|
|
|
|
|
+29. `WorkflowEditor.vue` LLM 配置面板:新增重试配置面板
|
|
|
|
|
+30. `RunHistory.vue`:适配 envelope
|
|
|
|
|
+31. 验证:`npm run build`
|
|
|
|
|
+
|
|
|
|
|
+### 阶段 7:回归测试
|
|
|
|
|
+32. 更新 `NodeInputResolverTest`:mock 数据改为 envelope 结构
|
|
|
|
|
+33. 新增 `NodeOutputEnvelopeTest`、`OutputValidatorTest`、`RetryPolicyTest`
|
|
|
|
|
+34. `mvn test` 全量通过
|
|
|
|
|
+35. `npm run build` 通过
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 四、风险与缓解
|
|
|
|
|
+
|
|
|
|
|
+| 风险 | 等级 | 缓解 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| LLM 强制 JSON 模式可能拒绝遵循(小模型) | 中 | 重试 + 类型转换容错;保留单输出降级路径 |
|
|
|
|
|
+| 重试导致工作流耗时翻倍 | 中 | 全局默认 2 次;用户可节点级关闭;监控 attempts 指标 |
|
|
|
|
|
+| NodeInputResolver 五级匹配改造引入 bug | 高 | 保留 legacy 兼容分支 + 完整单元测试 |
|
|
|
|
|
+| SmartAction 新增 outputs 声明兼容旧工作流 | 中 | 检测 `data.outputs` 是否存在,无声明走降级模式(旧工作流不破坏) |
|
|
|
|
|
+| Hermes 执行器与内置执行器双路径需同步 | 低 | 提取共享逻辑到工具类 |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 五、不变项(无需改动)
|
|
|
|
|
+
|
|
|
|
|
+- `ContextPromptHelper`(取 workspace.getVariable,已由 NodeWorkspaceBuilder 解包)
|
|
|
|
|
+- `TemplateRenderer`(同上)
|
|
|
|
|
+- `JsonPathExtractor` / `VariableConverter`(纯工具)
|
|
|
|
|
+- 前端 `ioInference.js` / `useNodeFields.js`(静态推断)
|
|
|
|
|
+- 画布节点组件(基于 `data.outputs/inputs` 静态渲染)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 六、验收标准
|
|
|
|
|
+
|
|
|
|
|
+1. 单元测试:`NodeOutputEnvelopeTest` / `OutputValidatorTest` / `RetryPolicyTest` / `StructuredOutputHelperTest` 全部通过
|
|
|
|
|
+2. 单元测试:`NodeInputResolverTest` 在 envelope 结构下正确解析
|
|
|
|
|
+3. 单元测试:`mvn test` 全量通过
|
|
|
|
|
+4. 构建:`mvn compile` 通过
|
|
|
|
|
+5. 构建:`npm run build` 通过
|
|
|
|
|
+6. 端到端(用户手动测试):
|
|
|
|
|
+ - LLM 多输出声明:LLM 按字段产出 JSON,下游能解析 data 字段
|
|
|
|
|
+ - LLM 故意返回错误 JSON:自动重试,最终失败时 status=400 + message 包含原因
|
|
|
|
|
+ - SmartAction 声明 outputs:按字段产出
|
|
|
|
|
+ - RunHistory 详情:节点输出展示 data 字段,失败节点显示 status 徽章
|