diff --git a/frontend/.cursor/rules/project.mdc b/frontend/.cursor/rules/project.mdc deleted file mode 100644 index c3ada895..00000000 --- a/frontend/.cursor/rules/project.mdc +++ /dev/null @@ -1,245 +0,0 @@ ---- -alwaysApply: true ---- -# 身份定义 -你是一位资深的软件架构师和工程师,具备丰富的项目经验和系统思维能力。你的核心优势在于: - -- 上下文工程专家:构建完整的任务上下文,而非简单的提示响应 -- 规范驱动思维:将模糊需求转化为精确、可执行的规范 -- 质量优先理念:每个阶段都确保高质量输出 -- 项目对齐能力:深度理解现有项目架构和约束 - -# 6A工作流执行规则 - -## 阶段1: Align (对齐阶段) -**目标:** 模糊需求 → 精确规范 - -### 执行步骤 - -### 1. 项目上下文分析 - -- 分析现有项目结构、技术栈、架构模式、依赖关系 -- 分析现有代码模式、现有文档和约定 -- 理解业务域和数据模型 - -### 2. 需求理解确认 - -- 创建 docs/任务名/ALIGNMENT_[任务名].md -- 包含项目和任务特性规范 -- 包含原始需求、边界确认(明确任务范围)、需求理解(对现有项目的理解)、疑问澄清(存在歧义的地方) - -### 3. 智能决策策略 - -- 自动识别歧义和不确定性 -- 生成结构化问题清单(按优先级排序) -- 优先基于现有项目内容和查找类似工程和行业知识进行决策和在文档中回答 -- 有人员倾向或不确定的问题主动中断并询问关键决策点 -- 基于回答更新理解和规范 - -### 4. 中断并询问关键决策点 - -- 主动中断询问,迭代执行智能决策策略 - -### 5. 最终共识 - -生成 docs/任务名/CONSENSUS_[任务名].md 包含: - -- 明确的需求描述和验收标准 -- 技术实现方案和技术约束和集成方案 -- 任务边界限制和验收标准 -- 确认所有不确定性已解决 - -### 质量门控 - -- 需求边界清晰无歧义 -- 技术方案与现有架构对齐 -- 验收标准具体可测试 -- 所有关键假设已确认 -- 项目特性规范已对齐 - -## 阶段2: Architect (架构阶段) -**目标: **共识文档 → 系统架构 → 模块设计 → 接口规范 - -### 执行步骤 - -### 1. 系统分层设计 - -基于CONSENSUS、ALIGNMENT文档设计架构 - -生成 docs/任务名/DESIGN_[任务名].md 包含: - -- 整体架构图(mermaid绘制) -- 分层设计和核心组件 -- 模块依赖关系图 -- 接口契约定义 -- 数据流向图 -- 异常处理策略 - -### 2. 设计原则 - -- 严格按照任务范围,避免过度设计 -- 确保与现有系统架构一致 -- 复用现有组件和模式 - -### 质量门控 - -- 架构图清晰准确 -- 接口定义完整 -- 与现有系统无冲突 -- 设计可行性验证 - -## 阶段3: Atomize (原子化阶段) - -**目标:** 架构设计 → 拆分任务 → 明确接口 → 依赖关系 - -### 执行步骤 - -### 1. 子任务拆分 - -基于DESIGN文档生成 docs/任务名/TASK_[任务名].md - -每个原子任务包含: - -- 输入契约(前置依赖、输入数据、环境依赖) -- 输出契约(输出数据、交付物、验收标准) -- 实现约束(技术栈、接口规范、质量要求) -- 依赖关系(后置任务、并行任务) - -### 2. 拆分原则 - -- 复杂度可控,便于AI高成功率交付 -- 按功能模块分解,确保任务原子性和独立性 -- 有明确的验收标准,尽量可以独立编译和测试 -- 依赖关系清晰 - -### 3. 生成任务依赖图(使用mermaid) - -### 质量门控 - -- 任务覆盖完整需求 -- 依赖关系无循环 -- 每个任务都可独立验证 -- 复杂度评估合理 - -## 阶段4: Approve (审批阶段) -**目标:** 原子任务 → 人工审查 → 迭代修改 → 按文档执行 - -### 执行步骤 - -### 1. 执行检查清单 - -- 完整性:任务计划覆盖所有需求 -- 一致性:与前期文档保持一致 -- 可行性:技术方案确实可行 -- 可控性:风险在可接受范围,复杂度是否可控 -- 可测性:验收标准明确可执行 - -### 2. 最终确认清单 - -- 明确的实现需求(无歧义) -- 明确的子任务定义 -- 明确的边界和限制 -- 明确的验收标准 -- 代码、测试、文档质量标准 - -## 阶段5: Automate (自动化执行) -**目标:** 按节点执行 → 编写测试 → 实现代码 → 文档同步 - -### 执行步骤 - -### 1. 逐步实施子任务 - -- 创建 docs/任务名/ACCEPTANCE_[任务名].md 记录完成情况 - -### 2. 代码质量要求 - -- 严格遵循项目现有代码规范 -- 保持与现有代码风格一致 -- 使用项目现有的工具和库 -- 复用项目现有组件 -- 代码尽量精简易读 -- API KEY放到.env文件中并且不要提交git - -### 3. 异常处理 - -- 遇到不确定问题立刻中断执行 -- 在TASK文档中记录问题详细信息和位置 -- 寻求人工澄清后继续 - -### 4. 逐步实施流程 按任务依赖顺序执行,对每个子任务执行: - -- 执行前检查(验证输入契约、环境准备、依赖满足) -- 实现核心逻辑(按设计文档编写代码) -- 编写单元测试(边界条件、异常情况) -- 运行验证测试 -- 更新相关文档 -- 每完成一个任务立即验证 - -## 阶段6: Assess (评估阶段) -**目标:** 执行结果 → 质量评估 → 文档更新 → 交付确认 - -### 执行步骤 - -### 1. 验证执行结果 - -更新 docs/任务名/ACCEPTANCE_[任务名].md - -整体验收检查: - -- 所有需求已实现 -- 验收标准全部满足 -- 项目编译通过 -- 所有测试通过 -- 功能完整性验证 -- 实现与设计文档一致 - -### 2. 质量评估指标 - -- 代码质量(规范、可读性、复杂度) -- 测试质量(覆盖率、用例有效性) -- 文档质量(完整性、准确性、一致性) -- 现有系统集成良好 -- 未引入技术债务 - -### 3. 最终交付物 - -- 生成 docs/任务名/FINAL_[任务名].md(项目总结报告) -- 生成 docs/任务名/TODO_[任务名].md(精简明确哪些待办的事宜和哪些缺少的配置等,我方便直接寻找支持) - -### 4. TODO询问 询问用户TODO的解决方式,精简明确哪些待办的事宜和哪些缺少的配置等,同时提供有用的操作指引 - -## 技术执行规范 - -### 安全规范 - -API密钥等敏感信息使用.env文件管理 - -### 文档同步 - -代码变更同时更新相关文档 - -### 测试策略 -**- 测试优先:**先写测试,后写实现 -**- 边界覆盖:**覆盖正常流程、边界条件、异常情况 - -## 交互体验优化 - -## 进度反馈 -- 显示当前执行阶段 -- 提供详细的执行步骤 -- 标示完成情况 -- 突出需要关注的问题 - -## 异常处理机制 - -### 中断条件 -- 遇到无法自主决策的问题 -- 觉得需要询问用户的问题 -- 技术实现出现阻塞 -- 文档不一致需要确认修正 - -### 恢复策略 -- 保存当前执行状态 -- 记录问题详细信息 -- 询问并等待人工干预 -- 从中断点任务继续执行 diff --git a/frontend/docs/project1.mdc b/frontend/docs/project1.mdc deleted file mode 100644 index 658cf8a3..00000000 --- a/frontend/docs/project1.mdc +++ /dev/null @@ -1,394 +0,0 @@ ---- -alwaysApply: true ---- ---- -alwaysApply: true ---- -# RIPER-5 + O1 THINKING + AGENT EXECUTION PROTOCOL (OPTIMIZED) - -## 目录 -- [RIPER-5 + O1 THINKING + AGENT EXECUTION PROTOCOL (OPTIMIZED)](#riper-5--o1-thinking--agent-execution-protocol-optimized) - - [目录](#目录) - - [上下文与设置](#上下文与设置) - - [核心思维原则](#核心思维原则) - - [模式详解](#模式详解) - - [模式1: RESEARCH](#模式1-research) - - [模式2: INNOVATE](#模式2-innovate) - - [模式3: PLAN](#模式3-plan) - - [模式4: EXECUTE](#模式4-execute) - - [模式5: REVIEW](#模式5-review) - - [关键协议指南](#关键协议指南) - - [代码处理指南](#代码处理指南) - - [任务文件模板](#任务文件模板) - - [性能期望](#性能期望) - -## 上下文与设置 - - -你是超智能AI编程助手,集成在Cursor IDE中(一个基于VS Code的AI增强IDE)。由于你的先进能力,你经常过于热衷于在未经明确请求的情况下实现更改,这可能导致代码逻辑破坏。为防止这种情况,你必须严格遵循本协议。 - -**语言设置**:除非用户另有指示,所有常规交互响应应使用中文。然而,模式声明(如[MODE: RESEARCH])和特定格式化输出(如代码块、检查清单等)应保持英文以确保格式一致性。 - -**自动模式启动**:本优化版支持自动启动所有模式,无需显式过渡命令。每个模式完成后将自动进入下一个模式。 - -**模式声明要求**:你必须在每个响应的开头以方括号声明当前模式,没有例外。格式:`[MODE: MODE_NAME]` - -**初始默认模式**:除非另有指示,每次新对话默认从RESEARCH模式开始。然而,如果用户的初始请求非常明确地指向特定阶段(例如,提供了一个完整的计划要求执行),可以直接进入相应的模式(如 EXECUTE)。 - -**代码修复指令**:请修复所有预期表达式问题,从第x行到第y行,请确保修复所有问题,不要遗漏任何问题。 - -## 核心思维原则 - - -在所有模式中,这些基本思维原则将指导你的操作: - -- **系统思维**:从整体架构到具体实现进行分析 -- **辩证思维**:评估多种解决方案及其利弊 -- **创新思维**:打破常规模式,寻求创新解决方案 -- **批判思维**:从多角度验证和优化解决方案 - -在所有响应中平衡这些方面: -- 分析与直觉 -- 细节检查与全局视角 -- 理论理解与实际应用 -- 深度思考与前进动力 -- 复杂性与清晰度 - -## 模式详解 - - -### 模式1: RESEARCH - - -**目的**:信息收集和深入理解 - -**核心思维应用**: -- 系统性地分解技术组件 -- 清晰地映射已知/未知元素 -- 考虑更广泛的架构影响 -- 识别关键技术约束和需求 - -**允许**: -- 阅读文件 -- 提出澄清问题 -- 理解代码结构 -- 分析系统架构 -- 识别技术债务或约束 -- 创建任务文件(参见下方任务文件模板) -- 使用文件工具创建或更新任务文件的‘Analysis’部分 - -**禁止**: -- 提出建议 -- 实施任何改变 -- 规划 -- 任何行动或解决方案的暗示 - -**研究协议步骤**: -1. 分析与任务相关的代码: - - 识别核心文件/功能 - - 追踪代码流程 - - 记录发现以供后续使用 - -**思考过程**: -```md -嗯... [系统思维方法的推理过程] -``` - -**输出格式**: -以[MODE: RESEARCH]开始,然后仅提供观察和问题。 -使用markdown语法格式化答案。 -除非明确要求,否则避免使用项目符号。 - -**持续时间**:自动在完成研究后进入INNOVATE模式 - -### 模式2: INNOVATE - - -**目的**:头脑风暴潜在方法 - -**核心思维应用**: -- 运用辩证思维探索多种解决路径 -- 应用创新思维打破常规模式 -- 平衡理论优雅与实际实现 -- 考虑技术可行性、可维护性和可扩展性 - -**允许**: -- 讨论多种解决方案想法 -- 评估优点/缺点 -- 寻求方法反馈 -- 探索架构替代方案 -- 在"提议的解决方案"部分记录发现 -- 使用文件工具更新任务文件的‘Proposed Solution’部分 - -**禁止**: -- 具体规划 -- 实现细节 -- 任何代码编写 -- 承诺特定解决方案 - -**创新协议步骤**: -1. 基于研究分析创建方案: - - 研究依赖关系 - - 考虑多种实现方法 - - 评估每种方法的利弊 - - 添加到任务文件的"提议的解决方案"部分 -2. 暂不进行代码更改 - -**思考过程**: -```md -嗯... [创造性、辩证的推理过程] -``` - -**输出格式**: -以[MODE: INNOVATE]开始,然后仅提供可能性和考虑事项。 -以自然流畅的段落呈现想法。 -保持不同解决方案元素之间的有机联系。 - -**持续时间**:自动在完成创新阶段后进入PLAN模式 - -### 模式3: PLAN - - -**目的**:创建详尽的技术规范 - -**核心思维应用**: -- 应用系统思维确保全面的解决方案架构 -- 使用批判思维评估和优化计划 -- 制定彻底的技术规范 -- 确保目标专注,将所有计划与原始需求连接起来 - -**允许**: -- 带有确切文件路径的详细计划 -- 精确的函数名称和签名 -- 具体的更改规范 -- 完整的架构概述 - -**禁止**: -- 任何实现或代码编写 -- 甚至"示例代码"也不可实现 -- 跳过或简化规范 - -**规划协议步骤**: -1. 查看"任务进度"历史(如果存在) -2. 详细规划下一步更改 -3. 提供明确理由和详细说明: - ``` - [更改计划] - - 文件:[更改的文件] - - 理由:[解释] - ``` - -**所需规划元素**: -- 文件路径和组件关系 -- 函数/类修改及其签名 -- 数据结构更改 -- 错误处理策略 -- 完整依赖管理 -- 测试方法 - -**强制最终步骤**: -将整个计划转换为编号的、按顺序排列的检查清单,每个原子操作作为单独的项目 - -**检查清单格式**: -``` -实施检查清单: -1. [具体操作1] -2. [具体操作2] -... -n. [最终操作] -``` - -**输出格式**: -以[MODE: PLAN]开始,然后仅提供规范和实现细节。 -使用markdown语法格式化答案。 - -**持续时间**:自动在计划完成后进入EXECUTE模式 - -### 模式4: EXECUTE - - -**目的**:完全按照模式3中的计划实施 - -**核心思维应用**: -- 专注于精确实现规范 -- 在实现过程中应用系统验证 -- 保持对计划的精确遵守 -- 实现完整功能,包括适当的错误处理 - -**允许**: -- 仅实现已在批准的计划中明确详述的内容 -- 严格按照编号的检查清单执行 -- 标记已完成的检查清单项目 -- 在实现后更新"任务进度"部分(这是执行过程的标准部分,被视为计划的内置步骤) - -**禁止**: -- 任何偏离计划的行为 -- 计划中未规定的改进 -- 创意补充或"更好的想法" -- 跳过或简化代码部分 - -**执行协议步骤**: -1. 完全按计划实施更改 -2. 在每次实施后,**使用文件工具**追加到"任务进度"(作为计划执行的标准步骤): - ``` - [日期时间] - - 修改:[文件和代码更改列表] - - 更改:[更改的摘要] - - 原因:[更改的原因] - - 阻碍:[阻止此更新成功的因素列表] - - 状态:[未确认|成功|失败] - ``` -3. 要求用户确认:"状态:成功/失败?" -4. 如果失败:返回PLAN模式 -5. 如果成功且需要更多更改:继续下一项 -6. 如果所有实施完成:进入REVIEW模式 - -**代码质量标准**: -- 始终显示完整代码上下文 -- 在代码块中指定语言和路径 -- 适当的错误处理 -- 标准化命名约定 -- 清晰简洁的注释 -- 格式:```language:file_path - -**偏差处理**: -如果发现任何需要偏离的问题,立即返回PLAN模式 - -**输出格式**: -以[MODE: EXECUTE]开始,然后仅提供与计划匹配的实现。 -包括已完成的检查清单项目。 - -### 模式5: REVIEW - - -**目的**:无情地验证实施与计划的一致性 - -**核心思维应用**: -- 应用批判思维验证实施的准确性 -- 使用系统思维评估对整个系统的影响 -- 检查意外后果 -- 验证技术正确性和完整性 - -**允许**: -- 计划与实施之间的逐行比较 -- 对已实现代码的技术验证 -- 检查错误、缺陷或意外行为 -- 根据原始需求进行验证 - -**要求**: -- 明确标记任何偏差,无论多么微小 -- 验证所有检查清单项目是否正确完成 -- 检查安全隐患 -- 确认代码可维护性 - -**审查协议步骤**: -1. 根据计划验证所有实施 -2. **使用文件工具**完成任务文件中的"最终审查"部分 - -**偏差格式**: -`检测到偏差:[确切偏差描述]` - -**报告**: -必须报告实施是否与计划完全一致 - -**结论格式**: -`实施与计划完全匹配` 或 `实施偏离计划` - -**输出格式**: -以[MODE: REVIEW]开始,然后进行系统比较和明确判断。 -使用markdown语法格式化。 - -## 关键协议指南 - - -- 在每个响应的开头声明当前模式 -- 在EXECUTE模式中,必须100%忠实地执行计划 -- 在REVIEW模式中,必须标记即使是最小的偏差 -- 你必须将分析深度与问题重要性相匹配 -- 你必须保持与原始需求的明确联系 -- 除非特别要求,否则禁用表情符号输出 -- 本优化版支持自动模式转换,无需明确过渡信号 - -## 代码处理指南 - - -**代码块结构**: -根据不同编程语言的注释语法选择适当的格式: - -风格语言(C、C++、Java、JavaScript、Go、Python、vue等等前后端语言): -```language:file_path -// ... existing code ... -{{ modifications }} -// ... existing code ... -``` - -如果语言类型不确定,使用通用格式: -```language:file_path -[... existing code ...] -{{ modifications }} -[... existing code ...] -``` - -**编辑指南**: -- 仅显示必要的修改 -- 包括文件路径和语言标识符 -- 提供上下文注释 -- 考虑对代码库的影响 -- 验证与请求的相关性 -- 保持范围合规性 -- 避免不必要的更改 - -**禁止行为**: -- 使用未经验证的依赖项 -- 留下不完整的功能 -- 包含未测试的代码 -- 使用过时的解决方案 -- 在未明确要求时使用项目符号 -- 跳过或简化代码部分 -- 修改不相关的代码 -- 使用代码占位符 - -## 任务文件模板 - - -``` -# 上下文 -文件名:[任务文件名] -创建于:[日期时间] -创建者:[用户名] -Yolo模式:[YOLO模式] - -# 任务描述 -[用户完整任务描述] - -# 项目概述 -[用户输入的项目详情] - -⚠️ 警告:切勿修改此部分 ⚠️ -[本部分应包含RIPER-5协议规则的核心摘要,确保在执行过程中可以参考] -⚠️ 警告:切勿修改此部分 ⚠️ - -# 分析 -[代码调查结果] - -# 提议的解决方案 -[行动计划] - -# 当前执行步骤:"[步骤编号和名称]" -- 例如:"2. 创建任务文件" - -# 任务进度 -[带时间戳的更改历史] - -# 最终审查 -[完成后的总结] -``` - -## 性能期望 - - -- 响应延迟应最小化,理想情况下≤360000ms -- 最大化计算能力和令牌限制 -- 寻求本质洞察而非表面枚举 -- 追求创新思维而非习惯性重复 -- 突破认知限制,调动所有计算资源 \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index d1167a4a..4e5b68e0 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Package, Shield, @@ -231,7 +232,6 @@ const Dashboard: React.FC = () => { // 获取当前团队和环境 const currentTeam = teams.find(t => t.teamId === currentTeamId); const currentEnv = currentTeam?.environments.find(e => e.environmentId === currentEnvId); - const currentApps = currentEnv?.applications || []; if (loading) { return ; @@ -314,70 +314,99 @@ const Dashboard: React.FC = () => { ) : ( - - -
-
- 选择部署环境 - + setCurrentEnvId(Number(value))}> + {/* 现代化 TAB 头部 - 独立于 Card */} +
+
+
+
+

部署环境

- {currentEnv && currentEnv.requiresApproval && currentEnv.approvers.length > 0 ? ( - <> - - 审批人: {currentEnv.approvers.map((a) => a.realName).join('、')} - - ) : ( - 占位 + {currentEnv && currentEnv.requiresApproval && currentEnv.approvers.length > 0 && ( +
+ + + 需审批: {currentEnv.approvers.map((a) => a.realName).join('、')} + +
)}
- - - - {/* 应用列表 */} - {currentApps.length === 0 ? ( -
- -

暂无可部署应用

-

- 环境「{currentEnv?.environmentName}」暂未配置任何应用 -

-
- ) : ( -
- {currentApps.map((app) => ( - +
+
+ {currentTeam.environments.map((env) => ( + ))}
- )} - - +
+
+ + {/* 内容区域 */} + {currentTeam.environments.map((env) => ( + +
+ {/* 应用列表 */} + {env.applications.length === 0 ? ( + + +
+ +
+

暂无可部署应用

+

+ 环境「{env.environmentName}」暂未配置任何应用 +

+
+
+ ) : ( +
+ {env.applications.map((app) => ( + + ))} +
+ )} +
+
+ ))} + ) )}
diff --git a/frontend/src/pages/Workflow/Definition/List/types.ts b/frontend/src/pages/Workflow/Definition/List/types.ts index fa1135a2..972626a2 100644 --- a/frontend/src/pages/Workflow/Definition/List/types.ts +++ b/frontend/src/pages/Workflow/Definition/List/types.ts @@ -30,16 +30,22 @@ export interface WorkflowDefinitionGraph { } /** - * 工作流定义边 + * 工作流定义边(后端实际返回的格式) */ export interface WorkflowDefinitionEdge { id: string; - source: string; - target: string; - sourceHandle?: string; - targetHandle?: string; - type?: string; - label?: string; + from: string; // 后端使用 from 字段作为源节点ID + to: string; // 后端使用 to 字段作为目标节点ID + name?: string; // 边的显示名称 + config?: { // 边的配置信息 + type?: string; // 边类型(如 sequence) + condition?: { // 条件配置 + type: 'EXPRESSION' | 'DEFAULT'; + expression?: string; + priority: number; + }; + }; + vertices?: Array<{ x: number; y: number }>; // 边的拐点坐标 } export interface WorkflowDefinitionNode { diff --git a/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx b/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx index ab79119c..160afe94 100644 --- a/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx +++ b/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogBody } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; @@ -53,7 +53,8 @@ type EdgeConditionFormValues = z.infer; /** * 边条件配置弹窗 - * 使用 shadcn/ui Dialog + react-hook-form + * ⚠️ 重要规范:只用于配置网关节点的出口连线 + * 非网关节点的多分支场景请使用网关节点 */ const EdgeConfigModal: React.FC = ({ visible, @@ -64,9 +65,10 @@ const EdgeConfigModal: React.FC = ({ onOk, onCancel }) => { + // ✅ 所有 Hooks 必须在最顶部调用,不能放在条件语句后面 const { toast } = useToast(); const [conditionType, setConditionType] = useState<'EXPRESSION' | 'DEFAULT'>('EXPRESSION'); - + const form = useForm({ resolver: zodResolver(edgeConditionSchema), defaultValues: { @@ -96,31 +98,183 @@ const EdgeConfigModal: React.FC = ({ } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, edge?.id]); + + // ⚠️ 检查源节点是否为网关节点(Hooks 调用之后再判断) + const sourceNode = edge ? allNodes.find(n => n.id === edge.source) : null; + const isFromGateway = sourceNode?.data?.nodeType === 'GATEWAY_NODE'; + // ✅ 网关类型存储在 inputMapping 中,不是 configs 中 + const gatewayType = isFromGateway ? (sourceNode?.data?.inputMapping?.gatewayType || sourceNode?.data?.configs?.gatewayType) : null; + + // 获取该网关的所有出口连线 + const gatewayEdges = isFromGateway && edge + ? allEdges.filter(e => e.source === edge.source) + : []; + + // 检查是否已有默认分支 + const hasDefaultBranch = gatewayEdges.some(e => e.data?.condition?.type === 'DEFAULT' && e.id !== edge?.id); + + // 检查优先级是否重复 + const usedPriorities = gatewayEdges + .filter(e => e.id !== edge?.id && e.data?.condition?.type === 'EXPRESSION') + .map(e => e.data?.condition?.priority) + .filter((p): p is number => p !== undefined); + + // ⚠️ 如果源节点不是网关节点,显示引导信息 + if (!isFromGateway) { + return ( + !open && onCancel()}> + + + 规范化提示 + + 为了符合 BPMN 2.0 标准,我们采用网关节点来处理分支逻辑 + + + +
+
+
+ ℹ️ +
+

+ 多分支场景请使用网关节点 +

+

+ 网关节点提供了清晰的可视化分支逻辑,并且完全符合 BPMN 标准,便于: +

+
    +
  • 前后端数据结构一致(所见即所得)
  • +
  • 与主流工作流引擎(Camunda/Flowable)对接
  • +
  • 导出标准的 BPMN 2.0 XML
  • +
  • 支持复杂的分支逻辑(排他/并行/包容)
  • +
+
+
+
+ +
+

💡 如何使用网关节点?

+
    +
  1. 从左侧节点面板拖入"网关节点"
  2. +
  3. 双击网关节点,选择类型(排他/并行/包容)
  4. +
  5. 连接源节点到网关节点
  6. +
  7. 连接网关节点到目标节点(可多条)
  8. +
  9. 双击网关的出口连线,配置分支条件
  10. +
+
+
+ + + + + +
+
+ ); + } const handleSubmit = (values: EdgeConditionFormValues) => { if (!edge) return; + // ⚠️ 网关节点必须先配置类型 + if (isFromGateway && !gatewayType) { + toast({ + variant: 'destructive', + title: '网关节点未配置', + description: '请先关闭此弹窗,双击网关节点配置其类型(排他/并行/包容),然后再配置分支条件。', + duration: 5000, + }); + return; + } + + // 网关节点特殊验证 + if (isFromGateway && gatewayType) { + // 排他网关/包容网关:检查优先级重复 + if ((gatewayType === 'exclusiveGateway' || gatewayType === 'inclusiveGateway') && values.type === 'EXPRESSION') { + if (usedPriorities.includes(values.priority)) { + toast({ + variant: 'destructive', + title: '优先级冲突', + description: `优先级 ${values.priority} 已被其他分支使用。已使用的优先级:${usedPriorities.join(', ')}。请选择不同的优先级。`, + duration: 5000, + }); + return; + } + } + + // 检查默认分支唯一性 + if (values.type === 'DEFAULT' && hasDefaultBranch) { + toast({ + variant: 'destructive', + title: '默认分支已存在', + description: `该网关已有默认分支,一个${gatewayType === 'exclusiveGateway' ? '排他' : '包容'}网关只能有一个默认分支。请将此分支设置为"表达式"类型,并配置条件表达式。`, + duration: 6000, + }); + return; + } + + // 并行网关:不需要条件表达式(仅提示,不阻止) + if (gatewayType === 'parallelGateway' && values.type === 'EXPRESSION') { + toast({ + title: '💡 温馨提示', + description: '并行网关的所有分支都会同时执行,配置条件表达式不会影响执行逻辑。建议保持默认配置即可。', + duration: 4000, + }); + } + + // 排他网关/包容网关:检查是否配置了条件表达式 + if ((gatewayType === 'exclusiveGateway' || gatewayType === 'inclusiveGateway') && values.type === 'EXPRESSION') { + if (!values.expression || values.expression.trim() === '') { + toast({ + variant: 'destructive', + title: '条件表达式为空', + description: '请输入条件表达式,或选择"默认路径"类型。表达式格式:${上游节点名称.字段名 == \'值\'}', + duration: 5000, + }); + return; + } + } + } + // 转换 expression 中的显示名称为 UUID const uuidExpression = values.expression ? convertToUUID(values.expression, allNodes) : ''; - // 检查表达式是否包含变量引用 - if (values.type === 'EXPRESSION' && uuidExpression) { - const hasVariable = /\$\{[^}]+\}/.test(uuidExpression); - if (!hasVariable) { + // 检查表达式是否包含变量引用(排他/包容网关的表达式必须包含变量) + if (isFromGateway && (gatewayType === 'exclusiveGateway' || gatewayType === 'inclusiveGateway') && values.type === 'EXPRESSION') { + const hasVariable = /\$\{[^}]+\}/.test(uuidExpression); + if (!hasVariable) { toast({ - title: '提示', - description: '表达式建议包含变量引用,支持格式:${节点名称.字段名} 或 ${节点名称.字段名 == \'值\'}', + variant: 'destructive', + title: '条件表达式格式错误', + description: '表达式必须包含变量引用!正确格式:${上游节点名称.字段名 == \'值\'}。点击输入框可查看可用变量。', + duration: 6000, }); - } + return; } - - // 提交 UUID 格式的数据 - onOk(edge.id, { - ...values, - expression: uuidExpression - }); + } + + // ✅ 提交成功提示 + toast({ + title: '✅ 保存成功', + description: values.type === 'DEFAULT' + ? '已设置为默认分支' + : `已设置条件表达式,优先级:${values.priority}`, + duration: 2000, + }); + + // 提交 UUID 格式的数据 + onOk(edge.id, { + type: values.type, + expression: uuidExpression, + // 默认分支自动设置优先级为 999(最低优先级,最后执行) + priority: values.type === 'DEFAULT' ? 999 : values.priority + }); handleClose(); }; @@ -136,13 +290,150 @@ const EdgeConfigModal: React.FC = ({ 配置边条件 - 设置流程分支的条件表达式和优先级 + {isFromGateway ? ( + <> + 源节点为网关节点 + {gatewayType === 'exclusiveGateway' && '(排他网关):根据优先级选择唯一一条分支'} + {gatewayType === 'parallelGateway' && '(并行网关):所有分支同时执行,无需条件'} + {gatewayType === 'inclusiveGateway' && '(包容网关):根据条件选择多条分支'} + + ) : ( + '设置流程分支的条件表达式和优先级' + )} -
- - + {/* ⚠️ 关键配置提示:网关节点未配置类型 */} + {isFromGateway && !gatewayType && ( +
+
+ ⚠️ +
+

+ 网关节点尚未配置类型 +

+

+ 在配置分支条件之前,您需要先设置网关节点的类型。请按以下步骤操作: +

+
    +
  1. 关闭此弹窗
  2. +
  3. 双击画布上的网关节点(菱形图标)
  4. +
  5. 在配置面板中选择网关类型: +
      +
    • 排他网关(XOR):根据条件选择唯一一条分支(如:成功/失败)
    • +
    • 并行网关(AND):所有分支同时执行(如:同时通知多人)
    • +
    • 包容网关(OR):根据条件选择多条分支
    • +
    +
  6. +
  7. 保存网关配置后,再回来配置分支条件
  8. +
+
+
+
+ )} + + {/* 🔴 错误提示:多个默认分支(仅排他/包容网关,且用户选择 DEFAULT 类型时显示) */} + {isFromGateway && gatewayType && gatewayType !== 'parallelGateway' && hasDefaultBranch && conditionType === 'DEFAULT' && ( +
+
+ +
+

+ 已存在默认分支 +

+

+ 一个{gatewayType === 'exclusiveGateway' ? '排他' : '包容'}网关只能有一个默认分支!其他分支必须配置条件表达式。 +

+

+ 💡 建议操作:将此分支设置为"表达式"类型,并配置条件表达式。 +

+
+
+
+ )} + + {/* ℹ️ 网关规则说明 */} + {isFromGateway && gatewayType && ( +
+
+ ℹ️ +
+ {gatewayType === 'exclusiveGateway' && ( + <> +

排他网关规则:

+
    +
  • 按优先级顺序评估条件(数字越小越优先)
  • +
  • 第一个满足的条件分支被执行,其他分支跳过
  • +
  • 必须有一个默认分支(当所有条件都不满足时执行)
  • +
  • 已使用的优先级:{usedPriorities.length > 0 ? usedPriorities.join(', ') : '无'}
  • +
+
+

💡 条件表达式示例:

+ + ${'{上游节点名称.status == \'SUCCESS\'}'} + +
+ + )} + {gatewayType === 'parallelGateway' && ( + <> +

并行网关规则:

+
    +
  • 所有出口分支同时执行(Fork模式)
  • +
  • 不需要条件表达式(无条件执行)
  • +
  • 下游汇聚点等待所有分支完成(Join模式)
  • +
+
+

+ 💡 并行网关的所有边线可以保持默认配置(自动执行) +

+
+ + )} + {gatewayType === 'inclusiveGateway' && ( + <> +

包容网关规则:

+
    +
  • 所有满足条件的分支都会执行(可能是1条或多条)
  • +
  • 建议为每条分支设置条件表达式
  • +
  • 默认分支在所有条件不满足时执行
  • +
+
+

💡 条件表达式示例:

+ + ${'{上游节点.score >= 80}'} + +
+ + )} +
+
+
+ )} + + {/* 并行网关:直接显示说明,不允许配置条件 */} + {isFromGateway && gatewayType === 'parallelGateway' ? ( +
+
+ +
+

+ 并行网关分支(自动执行) +

+

+ 并行网关的所有分支会同时执行,无需配置条件表达式、优先级或默认路径。 +

+

+ 💡 此分支将自动执行,与其他分支并行运行(Fork 模式)。 +

+
+
+
+ ) : ( + + + ( @@ -190,72 +481,82 @@ const EdgeConfigModal: React.FC = ({ /> {conditionType === 'EXPRESSION' ? ( - ( - - 条件表达式 - - - - - 使用 JUEL 表达式语法,完整表达式需在 {'${}'} 内。选择变量后在 {'}'} 前添加比较逻辑(如 == 'SUCCESS')。支持比较运算符(==、!=、>、<)和逻辑运算符(&&、||) - - - - )} - /> + <> + ( + + 条件表达式 + + + + + 使用 JUEL 表达式语法,完整表达式需在 {'${}'} 内。选择变量后在 {'}'} 前添加比较逻辑(如 == 'SUCCESS')。支持比较运算符(==、!=、>、<)和逻辑运算符(&&、||) + + + + )} + /> + + ( + + 优先级 + + field.onChange(Number(e.target.value))} + /> + + + 数字越小优先级越高,按优先级顺序评估条件(1-999)。已使用:{usedPriorities.length > 0 ? usedPriorities.join(', ') : '无'} + + + + )} + /> + ) : ( -
- 默认路径:当没有其他条件分支满足时,将执行此路径 +
+

默认路径

+

+ 当所有条件分支都不满足时,将执行此路径。默认分支不需要配置条件表达式和优先级。 +

)} + + + )} + - ( - - 优先级 - - field.onChange(Number(e.target.value))} - /> - - - 数字越小优先级越高(1-999) - - - - )} - /> - - - - - - - + + + {/* 并行网关不需要确定按钮,只需要关闭 */} + {(!isFromGateway || gatewayType !== 'parallelGateway') && ( + + )} + ); diff --git a/frontend/src/pages/Workflow/Design/hooks/useWorkflowLoad.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowLoad.ts index 67298fd6..eced6706 100644 --- a/frontend/src/pages/Workflow/Design/hooks/useWorkflowLoad.ts +++ b/frontend/src/pages/Workflow/Design/hooks/useWorkflowLoad.ts @@ -78,8 +78,9 @@ export const useWorkflowLoad = () => { }) .map(edge => { const edgeConfig: any = edge.config || {}; - const condition = edgeConfig.condition || { type: 'DEFAULT', priority: 10 }; - const label = condition.expression || edge.name || ''; + // ⚠️ 不再设置默认值,边线初始状态为 undefined,由用户主动配置 + const condition = edgeConfig.condition; + const label = condition?.expression || edge.name || ''; return { id: edge.id, diff --git a/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts index 343b5220..dadfca08 100644 --- a/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts +++ b/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts @@ -3,6 +3,7 @@ import { message } from 'antd'; import * as definitionService from '../../Definition/List/service'; import type { FlowNode, FlowEdge } from '../types'; import { NodeType, isConfigurableNode } from '../nodes/types'; +import { getGatewayErrors, getGatewayWarnings } from '../utils/gatewayValidation'; interface WorkflowSaveData { nodes: FlowNode[]; @@ -17,7 +18,7 @@ interface WorkflowSaveData { * 简化的保存前验证 - 只检查必需的业务规则 * 连接层面的验证已在 FlowCanvas 的 isValidConnection 中处理 */ -const validateBeforeSave = (nodes: FlowNode[]): { valid: boolean; message?: string } => { +const validateBeforeSave = (nodes: FlowNode[], edges: FlowEdge[]): { valid: boolean; message?: string } => { // 1. 检查是否为空 if (nodes.length === 0) { return { valid: false, message: '流程图中没有任何节点,请至少添加一个节点' }; @@ -35,6 +36,38 @@ const validateBeforeSave = (nodes: FlowNode[]): { valid: boolean; message?: stri return { valid: false, message: '流程图中必须包含结束节点' }; } + // 4. ⚠️ 规范检查:禁止非网关节点有多个条件出口 + const nonGatewayNodes = nodes.filter(node => node.data.nodeType !== NodeType.GATEWAY_NODE); + for (const node of nonGatewayNodes) { + const outgoingEdges = edges.filter(e => e.source === node.id); + + // 检查是否有多个出口且配置了条件 + const conditionalEdges = outgoingEdges.filter(e => e.data?.condition?.type === 'EXPRESSION'); + if (conditionalEdges.length > 0 || outgoingEdges.length > 2) { + const nodeName = node.data.label || '未命名节点'; + return { + valid: false, + message: `节点「${nodeName}」存在多分支逻辑。为符合 BPMN 标准,请使用网关节点来处理分支。提示:在节点后插入"排他网关"或"并行网关"。` + }; + } + } + + // 5. 网关节点验证 + const gatewayErrors = getGatewayErrors(nodes, edges); + if (gatewayErrors.length > 0) { + return { + valid: false, + message: `网关节点配置错误:\n${gatewayErrors.join('\n')}` + }; + } + + // 6. 网关节点警告(不阻止保存,只提示) + const gatewayWarnings = getGatewayWarnings(nodes, edges); + if (gatewayWarnings.length > 0) { + console.warn('网关节点警告:', gatewayWarnings); + // 警告不阻止保存,只在控制台输出 + } + return { valid: true }; }; @@ -46,7 +79,7 @@ export const useWorkflowSave = () => { // 保存工作流数据 const saveWorkflow = useCallback(async (data: WorkflowSaveData): Promise => { // 保存前验证(只验证业务规则,连接验证在 isValidConnection 中) - const validationResult = validateBeforeSave(data.nodes); + const validationResult = validateBeforeSave(data.nodes, data.edges); if (!validationResult.valid) { message.error(validationResult.message || '工作流验证失败'); return false; @@ -72,19 +105,27 @@ export const useWorkflowSave = () => { ? node.data.nodeDefinition.outputs || [] : [] // ✅ 输出能力定义(直接从节点定义中获取) })), - edges: data.edges.map(edge => ({ - id: edge.id, - from: edge.source, // 后端使用from字段 - to: edge.target, // 后端使用to字段 - name: edge.data?.label || "", // 边的名称 - sourceHandle: edge.sourceHandle, // 保存源连接点 - targetHandle: edge.targetHandle, // 保存目标连接点 - config: { - type: "sequence", // 固定为sequence类型 - condition: edge.data?.condition // 保存边条件 - }, - vertices: [] // 暂时为空数组 - })) + edges: data.edges.map(edge => { + // 检查源节点是否为并行网关 + const sourceNode = data.nodes.find(n => n.id === edge.source); + const isFromParallelGateway = sourceNode?.data?.nodeType === 'GATEWAY_NODE' + && sourceNode?.data?.inputMapping?.gatewayType === 'parallelGateway'; + + return { + id: edge.id, + from: edge.source, // 后端使用from字段 + to: edge.target, // 后端使用to字段 + name: edge.data?.label || "", // 边的名称 + sourceHandle: edge.sourceHandle, // 保存源连接点 + targetHandle: edge.targetHandle, // 保存目标连接点 + config: { + type: "sequence", // 固定为sequence类型 + // ⚠️ 并行网关的边线不传递条件配置(BPMN 规范不允许) + condition: isFromParallelGateway ? undefined : edge.data?.condition + }, + vertices: [] // 暂时为空数组 + }; + }) }; // ✅ 透传模式:只传递业务字段,更新 graph diff --git a/frontend/src/pages/Workflow/Design/nodes/GatewayNode.tsx b/frontend/src/pages/Workflow/Design/nodes/GatewayNode.tsx new file mode 100644 index 00000000..00b3cf13 --- /dev/null +++ b/frontend/src/pages/Workflow/Design/nodes/GatewayNode.tsx @@ -0,0 +1,140 @@ +import {ConfigurableNodeDefinition, NodeType, NodeCategory, defineNodeOutputs} from './types'; + +/** + * 网关类型枚举(与后端 GatewayTypeEnums 保持一致) + */ +export enum GatewayType { + EXCLUSIVE = 'exclusiveGateway', // 排他网关(XOR)- 选择唯一一条分支 + PARALLEL = 'parallelGateway', // 并行网关(AND)- 所有分支同时执行 + INCLUSIVE = 'inclusiveGateway' // 包容网关(OR)- 选择多条分支 +} + +/** + * 网关节点定义 + * 用于流程分支控制,支持三种网关类型 + */ +export const GatewayNodeDefinition: ConfigurableNodeDefinition = { + nodeCode: "GATEWAY", + nodeName: "网关节点", + nodeType: NodeType.GATEWAY_NODE, + category: NodeCategory.GATEWAY, + description: "流程分支控制节点,支持排他、并行、包容三种模式", + + // 渲染配置 + renderConfig: { + shape: 'diamond', // 菱形 + size: {width: 80, height: 80}, + icon: { + type: 'emoji', + content: '◆', // 默认菱形,后续根据类型动态改变 + size: 28 + }, + theme: { + primary: '#8b5cf6', // 紫色 + secondary: '#7c3aed', + selectedBorder: '#3b82f6', + hoverBorder: '#8b5cf6', + gradient: ['#ffffff', '#ede9fe'] + }, + handles: { + input: true, + output: true // 支持多个输出连线 + }, + features: { + showBadge: true, + showHoverMenu: true + } + }, + + // 输入配置Schema + inputMappingSchema: { + type: "object", + title: "网关配置", + description: "配置网关类型和分支规则", + properties: { + gatewayType: { + type: "string", + title: "网关类型", + description: "选择网关的执行模式", + enum: ["exclusiveGateway", "parallelGateway", "inclusiveGateway"], + enumNames: [ + "排他网关(XOR)- 根据条件只执行一条分支", + "并行网关(AND)- 所有分支同时执行", + "包容网关(OR)- 根据条件执行多条分支" + ], + default: "exclusiveGateway" + } + }, + required: ["gatewayType"] + }, + + // 输出配置(网关节点只有基础输出) + outputs: defineNodeOutputs( + { + name: "selectedBranches", + title: "已选分支", + type: "array", + description: "实际执行的分支ID列表(排他网关返回1个,并行网关返回多个,包容网关返回1-N个)", + example: ["branch_1", "branch_2"], + required: true + }, + { + name: "gatewayType", + title: "网关类型", + type: "string", + description: "当前网关的类型", + example: "EXCLUSIVE", + required: true + }, + { + name: "evaluationResults", + title: "条件评估结果", + type: "object", + description: "各分支条件的评估结果(仅当启用日志时)", + example: { + "branch_1": true, + "branch_2": false + }, + required: false + } + ) +}; + +/** + * 根据网关类型获取对应的图标 + * @param gatewayType 网关类型 + * @returns emoji 图标 + */ +export const getGatewayIcon = (gatewayType: GatewayType | string): string => { + switch (gatewayType) { + case GatewayType.EXCLUSIVE: + return '✖️'; // X - 排他网关 + case GatewayType.PARALLEL: + return '➕'; // + - 并行网关 + case GatewayType.INCLUSIVE: + return '⭕'; // O - 包容网关 + default: + return '◆'; // 默认菱形 + } +}; + +/** + * 根据网关类型获取描述文本 + * @param gatewayType 网关类型 + * @returns 描述文本 + */ +export const getGatewayDescription = (gatewayType: GatewayType | string): string => { + switch (gatewayType) { + case GatewayType.EXCLUSIVE: + return '排他网关:根据条件选择唯一一条分支'; + case GatewayType.PARALLEL: + return '并行网关:所有分支同时执行'; + case GatewayType.INCLUSIVE: + return '包容网关:根据条件选择多条分支'; + default: + return '网关节点'; + } +}; + +// ✅ 不再需要单独的渲染组件,使用 BaseNode 即可 + diff --git a/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx b/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx index b2e0c24e..2c80c7fa 100644 --- a/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx @@ -246,6 +246,92 @@ const BaseNode: React.FC = ({ data, selected }) => { // 椭圆形节点(开始/结束)使用特殊布局 const isEllipse = config.shape === 'ellipse'; + // 菱形节点(网关)使用特殊布局 + const isDiamond = config.shape === 'diamond'; + + // 菱形节点渲染(网关节点) + // ⚠️ 网关节点使用 Card 布局(与普通节点保持一致),但顶部添加菱形装饰 + if (isDiamond) { + return ( +
+ {/* 输入连接点 */} + {config.handles.input && ( + + )} + + {/* 使用 shadcn Card 组件(与普通节点一致) */} + + {/* 顶部添加菱形装饰标识 */} +
+
+ + {config.icon.content} + +
+
+ + + {/* 图标 + 标题 */} +
+ {renderIcon()} + + {nodeData.label || definition.nodeName} + +
+
+ + {/* 输入/输出部分 */} + {(renderInputSection() || renderOutputSection()) && ( + + {/* 输入部分 */} + {renderInputSection()} + + {/* 输出部分 */} + {renderOutputSection()} + + )} +
+ + {/* 输出连接点 */} + {config.handles.output && ( + + )} + + {/* 配置徽章 */} + {renderBadge()} +
+ ); + } return (
diff --git a/frontend/src/pages/Workflow/Design/nodes/index.ts b/frontend/src/pages/Workflow/Design/nodes/index.ts index 23df4d49..c4dc9c41 100644 --- a/frontend/src/pages/Workflow/Design/nodes/index.ts +++ b/frontend/src/pages/Workflow/Design/nodes/index.ts @@ -10,6 +10,7 @@ import { EndEventNodeDefinition } from './EndEventNode'; import { JenkinsBuildNodeDefinition } from './JenkinsBuildNode'; import { NotificationNodeDefinition } from './NotificationNode'; import { ApprovalNodeDefinition } from './ApprovalNode'; +import { GatewayNodeDefinition } from './GatewayNode'; import type { WorkflowNodeDefinition } from './types'; /** @@ -21,6 +22,7 @@ export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [ JenkinsBuildNodeDefinition, NotificationNodeDefinition, ApprovalNodeDefinition, + GatewayNodeDefinition, ]; /** @@ -33,6 +35,7 @@ export const nodeTypes = { JENKINS_BUILD: BaseNode, NOTIFICATION: BaseNode, APPROVAL: BaseNode, + GATEWAY_NODE: BaseNode, }; /** @@ -48,6 +51,7 @@ export { JenkinsBuildNodeDefinition, NotificationNodeDefinition, ApprovalNodeDefinition, + GatewayNodeDefinition, }; // 导出类型 diff --git a/frontend/src/pages/Workflow/Design/utils/gatewayValidation.ts b/frontend/src/pages/Workflow/Design/utils/gatewayValidation.ts new file mode 100644 index 00000000..c40d74e4 --- /dev/null +++ b/frontend/src/pages/Workflow/Design/utils/gatewayValidation.ts @@ -0,0 +1,160 @@ +import type { FlowNode, FlowEdge } from '../types'; + +/** + * 网关节点验证结果 + */ +export interface GatewayValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} + +/** + * 验证网关节点配置 + * @param node 网关节点 + * @param edges 所有边 + * @returns 验证结果 + */ +export const validateGatewayNode = ( + node: FlowNode, + edges: FlowEdge[] +): GatewayValidationResult => { + const errors: string[] = []; + const warnings: string[] = []; + + // 只验证网关节点 + if (node.data.nodeType !== 'GATEWAY_NODE') { + return { valid: true, errors: [], warnings: [] }; + } + + const gatewayType = node.data.configs?.gatewayType; + const nodeName = node.data.label || '网关节点'; + + // 获取出口连线 + const outgoingEdges = edges.filter(e => e.source === node.id); + + // 1. 检查出口连线数量 + if (outgoingEdges.length < 2) { + errors.push(`${nodeName}:网关节点至少需要2条出口连线(当前${outgoingEdges.length}条)`); + return { valid: false, errors, warnings }; + } + + // 2. 排他网关 / 包容网关特殊验证 + if (gatewayType === 'exclusiveGateway' || gatewayType === 'inclusiveGateway') { + // 检查默认分支 + const defaultBranches = outgoingEdges.filter(e => e.data?.condition?.type === 'DEFAULT'); + if (defaultBranches.length === 0) { + errors.push(`${nodeName}:${gatewayType === 'exclusiveGateway' ? '排他' : '包容'}网关必须有一个默认分支`); + } else if (defaultBranches.length > 1) { + errors.push(`${nodeName}:只能有一个默认分支(当前${defaultBranches.length}个)`); + } + + // 检查条件分支 + const expressionBranches = outgoingEdges.filter(e => e.data?.condition?.type === 'EXPRESSION'); + + if (gatewayType === 'exclusiveGateway') { + // 排他网关:检查优先级重复 + const priorities = expressionBranches.map(e => e.data?.condition?.priority); + const uniquePriorities = new Set(priorities); + if (priorities.length !== uniquePriorities.size) { + errors.push(`${nodeName}:排他网关的分支优先级不能重复`); + } + + // 检查是否所有条件分支都有优先级 + const noPriorityBranches = expressionBranches.filter(e => + e.data?.condition?.priority === undefined || + e.data?.condition?.priority === null + ); + if (noPriorityBranches.length > 0) { + errors.push(`${nodeName}:有${noPriorityBranches.length}条分支未设置优先级`); + } + } + + // 检查条件表达式是否为空 + const emptyExpressionBranches = expressionBranches.filter(e => + !e.data?.condition?.expression || + e.data?.condition?.expression.trim() === '' + ); + if (emptyExpressionBranches.length > 0) { + warnings.push(`${nodeName}:有${emptyExpressionBranches.length}条分支的条件表达式为空`); + } + } + + // 3. 并行网关验证 + if (gatewayType === 'PARALLEL') { + // 并行网关不需要条件,但可以给出提示 + const expressionBranches = outgoingEdges.filter(e => e.data?.condition?.type === 'EXPRESSION'); + if (expressionBranches.length > 0) { + warnings.push(`${nodeName}:并行网关的所有分支都会执行,条件表达式将被忽略`); + } + } + + // 4. 检查是否所有出口都有连接 + const unconnectedEdges = outgoingEdges.filter(e => !e.target); + if (unconnectedEdges.length > 0) { + errors.push(`${nodeName}:有${unconnectedEdges.length}条出口连线未连接到目标节点`); + } + + return { + valid: errors.length === 0, + errors, + warnings + }; +}; + +/** + * 验证所有网关节点 + * @param nodes 所有节点 + * @param edges 所有边 + * @returns 验证结果数组 + */ +export const validateAllGateways = ( + nodes: FlowNode[], + edges: FlowEdge[] +): GatewayValidationResult[] => { + const gatewayNodes = nodes.filter(n => n.data.nodeType === 'GATEWAY_NODE'); + return gatewayNodes.map(node => validateGatewayNode(node, edges)); +}; + +/** + * 检查工作流中是否存在网关验证错误 + * @param nodes 所有节点 + * @param edges 所有边 + * @returns 是否有错误 + */ +export const hasGatewayErrors = ( + nodes: FlowNode[], + edges: FlowEdge[] +): boolean => { + const results = validateAllGateways(nodes, edges); + return results.some(r => !r.valid); +}; + +/** + * 获取所有网关验证错误信息 + * @param nodes 所有节点 + * @param edges 所有边 + * @returns 错误信息数组 + */ +export const getGatewayErrors = ( + nodes: FlowNode[], + edges: FlowEdge[] +): string[] => { + const results = validateAllGateways(nodes, edges); + return results.flatMap(r => r.errors); +}; + +/** + * 获取所有网关验证警告信息 + * @param nodes 所有节点 + * @param edges 所有边 + * @returns 警告信息数组 + */ +export const getGatewayWarnings = ( + nodes: FlowNode[], + edges: FlowEdge[] +): string[] => { + const results = validateAllGateways(nodes, edges); + return results.flatMap(r => r.warnings); +}; +