diff --git a/src/api/flow/flow.js b/src/api/flow/flow.js index d8ea37b..b6eaf5c 100644 --- a/src/api/flow/flow.js +++ b/src/api/flow/flow.js @@ -1,6 +1,15 @@ import request from '@/utils/request' -// 发布流程 +/** + * 文件说明:流程设计器相关接口封装。 + * 约定:本文件只负责描述请求地址、请求方式和入参承载位置,不在这里处理业务返回结构。 + */ + +/** + * 发布流程。 + * @param {Object} data 发布请求体,通常包含流程 ID、流程 JSON、发布说明等后端要求字段。 + * @returns {Promise} 后端发布结果。 + */ export function flowDeploy(data) { return request({ url: '/flow/publish', @@ -9,18 +18,40 @@ export function flowDeploy(data) { }) } +/** + * 保存流程草稿。 + * 试运行前会先保存草稿,确保后端执行的是当前画布最新内容。 + * @param {Object} data 草稿数据,包含流程 ID、flowData、draftRevision 等。 + * @returns {Promise} 草稿保存结果,成功时后端会返回新的草稿版本号。 + */ export function flowSaveDraft(data) { return request({ url: '/flow/draft', method: 'put', data }) } +/** + * 查询某个流程的发布版本列表。 + * @param {string|number} itemId 流程定义 ID。 + * @returns {Promise} 发布记录列表。 + */ export function flowVersionList(itemId) { return request({ url: `/flow/versions/${itemId}`, method: 'get' }) } +/** + * 查询指定发布版本的完整详情。 + * @param {string|number} versionId 发布版本 ID。 + * @returns {Promise} 版本详情,通常包含版本号、发布说明和对应 flowData。 + */ export function flowVersionDetail(versionId) { return request({ url: `/flow/version/${versionId}`, method: 'get' }) } +/** + * 查询可作为子流程引用的已发布工作流。 + * currentItemId 用于后端过滤当前流程自身,以及会形成循环引用链路的工作流。 + * @param {string|number} currentItemId 当前正在编辑的流程 ID。 + * @returns {Promise} 可选子流程发布版本列表。 + */ export function flowSubFlowOptions(currentItemId) { return request({ url: '/flow/subflows', @@ -29,14 +60,31 @@ export function flowSubFlowOptions(currentItemId) { }) } +/** + * 将某个发布版本恢复为当前草稿。 + * @param {string|number} versionId 作为恢复来源的发布版本 ID。 + * @param {Object} data 恢复说明或后端要求的附加参数。 + * @returns {Promise} 恢复结果。 + */ export function flowRestoreDraft(versionId, data) { return request({ url: `/flow/version/${versionId}/restore-draft`, method: 'post', data }) } +/** + * 回退到某个发布版本。 + * @param {string|number} versionId 目标发布版本 ID。 + * @param {Object} data 回退说明或后端要求的附加参数。 + * @returns {Promise} 回退结果。 + */ export function flowRollbackVersion(versionId, data) { return request({ url: `/flow/version/${versionId}/rollback`, method: 'post', data }) } +/** + * 正式执行流程。 + * @param {Object} data 执行请求体,通常包含流程 ID、任务参数、资源分配等。 + * @returns {Promise} 执行启动结果。 + */ export function flowExecute(data) { return request({ url: '/flow/execute', @@ -45,6 +93,11 @@ export function flowExecute(data) { }) } +/** + * 执行流程运行过程中的控制或单节点动作。 + * @param {Object} data 动作请求体,具体字段由后端按 action 类型解析。 + * @returns {Promise} 动作执行结果。 + */ export function flowAction(data) { return request({ url: '/flow/action', @@ -53,6 +106,11 @@ export function flowAction(data) { }) } +/** + * 试运行流程。 + * @param {Object} data 试运行请求体,通常包含流程 ID、草稿版本、运行参数和资源选择。 + * @returns {Promise} 试运行实例创建结果。 + */ export function flowExecuteTrial(data) { return request({ url: '/flow/executeTrial', @@ -61,6 +119,11 @@ export function flowExecuteTrial(data) { }) } +/** + * 查询流程实例执行日志或节点状态快照。 + * @param {Object} data 查询条件,常用字段包括 itemId、instId、taskId。 + * @returns {Promise} 节点运行日志列表。 + */ export function flowView(data) { return request({ url: '/flow/view', @@ -69,6 +132,11 @@ export function flowView(data) { }) } +/** + * 暂停正在运行的流程实例。 + * @param {string|number} instId 流程实例 ID。 + * @returns {Promise} 暂停结果。 + */ export function flowPause(instId) { return request({ url: `/flow/pause/${instId}`, @@ -76,6 +144,11 @@ export function flowPause(instId) { }) } +/** + * 恢复已暂停的流程实例。 + * @param {string|number} instId 流程实例 ID。 + * @returns {Promise} 恢复结果。 + */ export function flowResume(instId) { return request({ url: `/flow/resume/${instId}`, @@ -83,6 +156,11 @@ export function flowResume(instId) { }) } +/** + * 终止正在运行或暂停中的流程实例。 + * @param {string|number} instId 流程实例 ID。 + * @returns {Promise} 终止结果。 + */ export function flowStop(instId) { return request({ url: `/flow/stop/${instId}`, @@ -90,6 +168,11 @@ export function flowStop(instId) { }) } +/** + * 查询机械臂当前位姿。 + * @param {Object} params 查询参数,通常包含机器人或设备标识。 + * @returns {Promise} 机械臂位姿信息。 + */ export function getArmStatus(params) { return request({ url: '/api/arm/getPose', @@ -98,6 +181,11 @@ export function getArmStatus(params) { }) } +/** + * 查询 AGV 底盘运行状态。 + * @param {Object} params 查询参数,通常包含机器人或设备标识。 + * @returns {Promise} AGV 运行状态。 + */ export function getAgvStatus(params) { return request({ url: '/api/agv/getRuntimeState', @@ -106,6 +194,11 @@ export function getAgvStatus(params) { }) } +/** + * 查询 AGV 可用站点列表。 + * @param {Object} params 查询参数,通常包含机器人或地图/设备信息。 + * @returns {Promise} 站点列表。 + */ export function getAgvStations(params) { return request({ url: '/api/agv/listStations', @@ -114,6 +207,11 @@ export function getAgvStations(params) { }) } +/** + * 查询机械臂关节状态。 + * @param {Object} params 查询参数,通常包含机器人或设备标识。 + * @returns {Promise} 机械臂关节角度/状态信息。 + */ export function getArmJointState(params) { return request({ url: '/api/arm/getJointState', diff --git a/src/utils/flow.js b/src/utils/flow.js index 5344c54..b837aba 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -1,3 +1,15 @@ +/** + * 文件说明:流程设计器通用工具函数。 + * 该文件大量依赖画布全局实例 lf,用于读取/修改当前流程图、生成参数引用树和转换节点表单数据。 + */ + +/** + * 生成当前画布所有可展示节点的输入/输出字段树。 + * 该函数只负责构造完整候选树,不判断节点是否位于当前节点上游;需要上游限制时使用 getUpstreamInput。 + * @param {string} nodeId 当前节点 ID,候选列表会排除该节点自身。 + * @param {boolean} isStrict 历史保留参数,旧逻辑曾用于要求节点必须存在入边;当前未启用。 + * @returns {Array} Element Plus Cascader 可使用的节点参数树。 + */ export const getInput = (nodeId, isStrict = true) => { const data = lf.getGraphData(); // let flag = false @@ -13,6 +25,7 @@ export const getInput = (nodeId, isStrict = true) => { // if (flag) { const arr = [] + // 结构节点、控制节点和终止节点不产生可供业务节点引用的有效输入/输出。 const filterNodes = ['sleep', 'stopLoop', 'subEnd', 'subStart', 'branch', 'end'] data.nodes.forEach(item => { if (item.id === nodeId) { @@ -23,6 +36,7 @@ export const getInput = (nodeId, isStrict = true) => { return } + // 每个节点下固定分为“输入”和“输出”两组,叶子节点由 transformTree 递归补齐。 const obj = { value: item.id, label: item.properties.name, @@ -55,6 +69,13 @@ export const getInput = (nodeId, isStrict = true) => { // return flag } +/** + * 将参数定义数组递归转换为级联选择器节点。 + * @param {Array} arr 参数定义数组,支持 children 嵌套。 + * @param {Array} parent 当前层级要写入的级联节点数组。 + * @param {"input"|"output"} type 当前参数所属分组,用于后续保存 quoteType。 + * @returns {Array} 转换后的 parent 数组。 + */ const transformTree = (arr, parent = [], type) => { arr.forEach(item => { const currentNode = { @@ -74,6 +95,10 @@ const transformTree = (arr, parent = [], type) => { return parent; }; +/** + * 读取开始节点的输入参数,并补充运行表单需要的默认值和 propPath。 + * @returns {Array} 已带 value/propPath 的开始参数数组。 + */ export const getStartNodeFormData = () => { const flowData = lf.getGraphData(); const startNode = flowData.nodes.find(item => item.type === 'start') @@ -81,6 +106,12 @@ export const getStartNodeFormData = () => { return data } +/** + * 给参数定义递归补充默认值 value 和表单校验路径 propPath。 + * @param {Object|Array} data 参数定义或参数定义数组。 + * @param {string} schemeId 特殊参数 schemeId 的默认值。 + * @returns {Object|Array} 原结构附加 value/propPath 后的结果。 + */ export const addValueProperty = (data, schemeId = '') => { // 处理单个对象或对象数组 const processItem = (item, index, path) => { @@ -105,6 +136,15 @@ export const addValueProperty = (data, schemeId = '') => { return Array.isArray(data) ? data.map((item, index) => processItem(item, index, '')) : processItem(data, 0, '') } +/** + * 递归查找某个节点所属的全部外层容器。 + * 常用于判断节点是否在循环、自定义分组等父级结构内。 + * @param {Array} data 流程节点数组。 + * @param {string} id 子节点 ID。 + * @param {string[]} type 需要匹配的父级节点类型。 + * @param {Array} result 递归累积结果。 + * @returns {Array} 从内到外匹配到的父级节点集合。 + */ export const recursiveFilter = (data, id, type = ['loop', 'customGroup'], result = []) => { data.forEach((item) => { if (type.includes(item.type) && item.children && item.children.includes(id)) { @@ -115,6 +155,13 @@ export const recursiveFilter = (data, id, type = ['loop', 'customGroup'], result return result } +/** + * 将树形表单字段转换为普通对象。 + * 叶子字段取 value;存在 children 的字段会递归生成嵌套对象。 + * @param {Array} arr 表单字段数组。 + * @param {Object} data 写入目标对象,递归时复用。 + * @returns {Object} 转换后的对象。 + */ export const formatTableData = (arr, data = {}) => { arr.forEach(item => { data[item.name] = item.value @@ -126,9 +173,14 @@ export const formatTableData = (arr, data = {}) => { return data } +/** + * 重新创建与指定节点相连的边。 + * 节点宽度或锚点位置变化后,删除再添加边可以触发画布重新计算连线位置。 + * @param {string} nodeId 需要刷新连线的节点 ID。 + */ export const addNewEdge = (nodeId) => { const { edges } = lf.getGraphData(); - const ttt = edges.slice(); // 浅拷贝(比 JSON 深拷贝高效 10x+) + const ttt = edges.slice(); // 浅拷贝即可保护遍历集合,避免删除边时影响原数组迭代。 const edgesToDelete = ttt.filter(_edge => _edge.sourceNodeId === nodeId || _edge.targetNodeId === nodeId @@ -140,6 +192,7 @@ export const addNewEdge = (nodeId) => { clearTimeout(window.addNewEdgeTimer); // 清理上一次未执行的定时器 } + // 延迟到 DOM 和节点尺寸更新后再添加边,避免仍使用旧锚点位置。 window.addNewEdgeTimer = setTimeout(() => { edgesToDelete.forEach(item => { lf.addEdge({ @@ -154,6 +207,11 @@ export const addNewEdge = (nodeId) => { }, 50) } +/** + * 立即刷新指定节点关联的边。 + * 与 addNewEdge 不同,这里不做防抖和延迟,适合无需等待 DOM 尺寸变化的场景。 + * @param {string} nodeId 需要刷新连线的节点 ID。 + */ export const newEdge = (nodeId) => { const { edges } = lf.getGraphData(); const arr = JSON.parse(JSON.stringify(edges)) @@ -172,6 +230,13 @@ export const newEdge = (nodeId) => { }) } +/** + * 初始化或切换节点的展开宽度,并在尺寸变化后刷新连线。 + * @param {string} modelId 节点 DOM class / 节点 ID。 + * @param {boolean} nodeZoom 是否展开为大宽度。 + * @param {string} className 用于查找 VueFlow 外层节点容器的选择器。 + * @param {boolean} init 是否为初始化阶段;初始化时不刷新边。 + */ export const initNodeZoom = (modelId, nodeZoom, className, init=false) => { const nodes = document.getElementsByClassName(modelId) if (nodes.length > 0) { @@ -188,6 +253,11 @@ export const initNodeZoom = (modelId, nodeZoom, className, init=false) => { } } +/** + * 收集当前节点存在入边时,画布中所有 number 类型输入参数。 + * @param {string} nodeId 当前节点 ID。 + * @returns {false|Array} 无入边时返回 false;有入边时返回 number 参数数组。 + */ export const getInputNumber = (nodeId) => { const data = lf.getGraphData(); let flag = false @@ -212,6 +282,12 @@ export const getInputNumber = (nodeId) => { return flag } +/** + * 删除分支节点某个出口锚点上的所有连线。 + * 删除条件分支时调用,避免已删除条件对应的边继续残留在画布上。 + * @param {string} nodeId 分支节点 ID。 + * @param {string} sourceAnchorId 分支出口锚点 ID。 + */ export const removeSwitchEdge = (nodeId, sourceAnchorId) => { const { edges } = lf.getGraphData(); const matchedEdges = edges.filter(item => { @@ -221,8 +297,15 @@ export const removeSwitchEdge = (nodeId, sourceAnchorId) => { matchedEdges.forEach(edge => lf.deleteEdge(edge.id)) } +/** + * 构建“子节点 ID -> 父容器 ID”的映射。 + * 统一兼容 VueFlow 当前 children 结构和旧流程保存的 properties.parentId 字段。 + * @param {Object} data 流程图数据,包含 nodes/edges。 + * @returns {Map} 子节点到父节点的映射。 + */ const buildParentByChild = (data) => { const result = new Map(); + // 兼容 VueFlow children 集合和旧数据中的 properties.parentId,两种来源都可标识容器归属。 ;(data.nodes || []).forEach((node) => { ;(node.children || []).forEach((childId) => result.set(String(childId), String(node.id))); }); @@ -232,6 +315,13 @@ const buildParentByChild = (data) => { return result; }; +/** + * 沿着有向边反向收集 targetId 的所有前置节点。 + * result 由调用方传入,便于在不同容器层级中重复累加可见上游节点。 + * @param {Object} data 流程图数据,包含 nodes/edges。 + * @param {string} targetId 目标节点 ID。 + * @param {Set} result 前置节点 ID 集合。 + */ const collectPredecessors = (data, targetId, result) => { const pending = [String(targetId)]; while (pending.length) { @@ -247,9 +337,12 @@ const collectPredecessors = (data, targetId, result) => { }; /** - * Returns nodes whose values are available before the target executes. Loop children - * may read earlier siblings and nodes before their loop, but never later children, - * another loop body, or nodes after the loop. + * 返回目标节点执行前已经可用的节点集合。 + * 循环体内节点只能引用同一循环体内的前置兄弟节点,以及循环容器外部的前置节点; + * 不能引用后置节点、其他循环体内部节点或循环之后才会执行的节点。 + * @param {Object} data 流程图数据。 + * @param {string} nodeId 目标节点 ID。 + * @returns {Set} 目标节点可引用的上游节点 ID 集合。 */ export const getUpstreamNodeIds = (data, nodeId) => { const graph = data || { nodes: [], edges: [] }; @@ -260,8 +353,7 @@ export const getUpstreamNodeIds = (data, nodeId) => { (graph.nodes || []).filter((node) => node.type === 'start').map((node) => String(node.id)) ); - // Walk from the node through every containing loop. At each level, only nodes in - // that exact scope are visible; this also supports legacy nested-loop workflows. + // 从当前节点逐级向外层容器回溯;每一层只接受同作用域内的前置节点,兼容历史嵌套循环数据。 let currentTargetId = targetId; let currentScopeId = parentByChild.get(targetId) || ''; const visitedScopes = new Set(); @@ -281,6 +373,12 @@ export const getUpstreamNodeIds = (data, nodeId) => { return result; }; +/** + * 生成级联选择器使用的上游入参/出参树。 + * 开始节点只暴露输入参数,普通节点只暴露输出参数,防止用户选到运行时不可用的字段。 + * @param {string} nodeId 当前节点 ID。 + * @returns {Array} 已按可执行顺序过滤后的级联选项树。 + */ export const getUpstreamInput = (nodeId) => { const data = lf.getGraphData(); const startIds = new Set( @@ -298,6 +396,12 @@ export const getUpstreamInput = (nodeId) => { .filter((option) => option.children.some((group) => group.children?.length)); } +/** + * 将扁平节点数组按 children 字段转换成树结构。 + * 节点关系弹窗使用该结果展示父子层级,例如循环容器内的子节点。 + * @param {Array} data 流程节点数组。 + * @returns {Array} 根节点数组,每个根节点递归带 children。 + */ export const convertToTree = (data) => { // 创建 id 到节点的映射 const nodeMap = new Map(); @@ -341,6 +445,12 @@ export const convertToTree = (data) => { } +/** + * 递归过滤 name 为空的字段定义。 + * 常用于保存输出参数前移除未填写名称的空行,避免生成不可引用的空字段。 + * @param {*} obj 字段定义、字段数组或基础类型。 + * @returns {*} 过滤后的结构;被剔除的对象返回 null。 + */ export const filterEmptyName = (obj) => { // 1. 如果是数组,遍历每一项递归处理 if (Array.isArray(obj)) { @@ -372,9 +482,10 @@ export const filterEmptyName = (obj) => { } /** - * 将原始表单配置数组 转换为 目标对象格式 - * @param {Array} source - 原始数据数组 - * @returns {Object} 转换后的目标格式对象 + * 将 HTTP 节点面板中的表单数组转换为后端执行协议。 + * config/headers/params 保留 children 数组;body 会展开为 { type, json, formData }。 + * @param {Array} source HTTP 节点原始表单配置数组。 + * @returns {Object} 后端执行 HTTP 请求所需的结构化配置。 */ export const transformHttpNodeData = (source) => { // 初始化结果对象 @@ -420,6 +531,12 @@ export const transformHttpNodeData = (source) => { return result; } +/** + * 将智能体节点表单数组转换为后端执行协议。 + * 普通配置字段保留 children;invokeTts 是开关值,需要从 item.input 提取为顶层字段。 + * @param {Array} source 智能体节点原始表单配置数组。 + * @returns {Object} 智能体调用配置。 + */ export const transformSdAgentNodeData = (source) => { // 初始化结果对象 const result = {}; @@ -444,6 +561,12 @@ export const transformSdAgentNodeData = (source) => { return result; } +/** + * 将识别节点表单转换为后端协议。 + * 普通字段写入 nodeParams;alarmRules 单独提取为告警规则数组,并兼容旧版 children 结构。 + * @param {Array} source 识别节点原始表单配置数组。 + * @returns {{nodeParams: Array, alarmRules: Array}} 识别节点配置。 + */ export const transformRecognizeNodeData = (source) => { // 初始化结果对象 const result = { diff --git a/src/utils/flowResources.js b/src/utils/flowResources.js index e5e9392..841191b 100644 --- a/src/utils/flowResources.js +++ b/src/utils/flowResources.js @@ -342,6 +342,10 @@ const collectActiveQuotes = (value, result = []) => { return result } +/** + * 对节点引用依赖做深度优先遍历,发现 A 引用 B、B 又回到 A 的循环链路。 + * states: 1 表示当前递归栈中,2 表示已确认无环;命中 1 时即可截取循环路径。 + */ const findReferenceCycle = (dependencies) => { const states = new Map() const stack = [] @@ -369,7 +373,10 @@ const findReferenceCycle = (dependencies) => { return null } -/** Validates persisted references as well as choices made through the parameter panels. */ +/** + * 校验已保存的引用配置和参数面板刚选择的引用配置。 + * 当前只阻断循环引用,避免节点输出依赖后续又间接依赖自身,导致运行端无法拓扑执行。 + */ export const validateFlowReferences = (graph) => { const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [] const nodeById = new Map(nodes.map((node) => [String(node.id), node])) @@ -395,6 +402,10 @@ export const validateFlowReferences = (graph) => { return [] } +/** + * 保存或发布前移除旧版物理机器人/设备 ID,只保留资源角色和设备用途定义。 + * 运行时会重新选择实际设备,避免把历史测试设备固化进流程模板。 + */ export const sanitizeFlowResourceDefinition = (sourceGraph) => { const graph = normalizeFlowResourceSchema(sourceGraph) const start = getStartNode(graph) @@ -408,6 +419,10 @@ export const sanitizeFlowResourceDefinition = (sourceGraph) => { return graph } +/** + * 粘贴跨流程节点时合并其依赖的机器人角色和设备用途。 + * 优先复用同标识或同名资源;找不到时才追加由流程节点托管的资源定义。 + */ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNodes = []) => { const graph = normalizeFlowResourceSchema(targetGraph) const start = getStartNode(graph) @@ -437,6 +452,7 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo targetRole ||= targetRoles[0] target.roleKey = targetRole.roleKey + // 设备用途可能只存在于来源流程,粘贴后需要映射到目标流程中可用的 slotKey。 const remapSlot = (slotKey) => { if (!slotKey || targetRole.deviceSlots?.some((slot) => slot.slotKey === slotKey)) return slotKey const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === slotKey) @@ -459,6 +475,7 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo [binding.parameterName, binding.slotKey] ))) const queueStepsParam = node.properties?.nodeParams?.find((param) => param.name === 'steps') + // 动作队列步骤内保存的是设备绑定名,合并资源后同步回真实 slotKey,单步执行才能定位设备。 if (Array.isArray(queueStepsParam?.input)) { queueStepsParam.input.forEach((step) => { if (bindingSlotByName.has(step?.deviceBindingKey)) { @@ -473,6 +490,10 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo export const getDeviceKindName = (deviceKind) => DEVICE_KIND_META[Number(deviceKind)]?.name || '任意设备' +/** + * 汇总主流程和子流程的资源契约,生成运行前需要一次性分配的角色集合。 + * 同一 roleKey 下的必选项取更严格配置,设备用途按 slotKey 合并。 + */ export const mergeResourceRoleDefinitions = (roleGroups = []) => { const merged = [] roleGroups.flat().forEach((sourceRole) => { diff --git a/src/views/flow/components/LoopIterationResults.vue b/src/views/flow/components/LoopIterationResults.vue index cb311a6..fe269b7 100644 --- a/src/views/flow/components/LoopIterationResults.vue +++ b/src/views/flow/components/LoopIterationResults.vue @@ -61,6 +61,8 @@ const props = defineProps({ const expandedIterations = ref([]) let initialized = false +// 首次拿到运行结果时只自动展开一次:优先展开正在执行的轮次,否则展开最新一轮。 +// 之后用户手动折叠/展开时不再被轮询数据刷新打断。 watch(() => props.history, (history) => { if (initialized || !history?.length) return const active = history.find(item => String(item.status || '').toUpperCase() === 'RUNNING') @@ -68,6 +70,7 @@ watch(() => props.history, (history) => { initialized = true }, { immediate: true, deep: true }) +// 后端状态枚举统一映射为 Element Plus tag 的文案和类型。 const statusMeta = (status) => { const value = String(status || '').toUpperCase() if (value === 'SUCCESS') return { label: '执行完成', type: 'success' } @@ -77,6 +80,7 @@ const statusMeta = (status) => { return { label: '执行中', type: 'warning' } } +// 循环轮次的分析结论独立于执行状态,主要服务音视频/图片分析类节点的通过性展示。 const analysisMeta = (status) => { if (status === 'PASSED') return { label: '分析通过', type: 'success' } if (status === 'FAILED') return { label: '分析未通过', type: 'danger' } @@ -84,6 +88,7 @@ const analysisMeta = (status) => { return null } +// 汇总所有轮次的分析状态,用于顶部快速提示是否存在未通过的循环结果。 const analysisSummary = computed(() => { const statuses = props.history.map(item => item.analysisStatus).filter(Boolean) if (!statuses.length) return null @@ -92,6 +97,7 @@ const analysisSummary = computed(() => { return { label: '全部通过', type: 'success' } }) +// 后端以毫秒返回耗时,短耗时保留 ms,较长耗时转秒,避免标题区域过长。 const formatDuration = (value) => { const duration = Number(value) if (!Number.isFinite(duration) || duration < 0) return '-' @@ -99,6 +105,7 @@ const formatDuration = (value) => { return `${(duration / 1000).toFixed(2)} s` } +// JSON Viewer 只在确实有内容时展示,避免空对象/空数组占据节点详情空间。 const hasValue = (value) => { if (value === null || value === undefined || value === '') return false if (Array.isArray(value)) return value.length > 0 diff --git a/src/views/flow/components/NodeState.vue b/src/views/flow/components/NodeState.vue index d53e966..8c11ff3 100644 --- a/src/views/flow/components/NodeState.vue +++ b/src/views/flow/components/NodeState.vue @@ -51,6 +51,7 @@ import { CircleCloseFilled, Loading, SuccessFilled, VideoPause, View } from "@el const props = defineProps({ state: { type: String, default: "NORMAL" }, runtimes: { type: Number, default: 0 }, + // 循环和子流程的运行结果更宽,允许调用方覆盖弹窗尺寸和样式类。 popoverWidth: { type: Number, default: 480 }, popperClass: { type: String, default: "flow-result-popover" }, }); diff --git a/src/views/flow/components/NodeTitle.vue b/src/views/flow/components/NodeTitle.vue index f8e4e76..8859e9e 100644 --- a/src/views/flow/components/NodeTitle.vue +++ b/src/views/flow/components/NodeTitle.vue @@ -91,6 +91,7 @@ const isEditing = ref(false); const localName = ref(""); const nameInputRef = ref(); const isSelectArea = computed(() => props.nodeType === "selectArea"); +// 这些动作只是流程结构或上下文占位,不能脱离完整流程单独执行。 const flowContextActions = new Set([ "NONE", "START", diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index d26cd31..81a2ff0 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -81,6 +81,7 @@ const currentComponent = computed(() => { return null }) +// 只有子流程参数面板需要当前流程 ID,用于后端过滤不可引用的发布版本。 const componentExtraProps = computed(() => ( props.data?.type === 'subFlow' ? { flowId: props.flowId } : {} )) diff --git a/src/views/flow/components/SubFlowExecutionResults.vue b/src/views/flow/components/SubFlowExecutionResults.vue index 54f6c45..44a8fc3 100644 --- a/src/views/flow/components/SubFlowExecutionResults.vue +++ b/src/views/flow/components/SubFlowExecutionResults.vue @@ -68,6 +68,8 @@ const executions = computed(() => Array.isArray(props.data.executions) ? props.d const expandedExecutions = ref([]) let initialized = false +// 子流程可能被轮询持续刷新,默认展开只做一次,避免用户查看历史分组时被新数据打断。 +// 若有正在运行的分组优先展开它,否则展开最新一次执行分组。 watch(executions, (groups) => { if (initialized || !groups.length) return const active = groups.find((group) => String(group.status || '').toUpperCase() === 'RUNNING') @@ -75,6 +77,7 @@ watch(executions, (groups) => { initialized = true }, { immediate: true }) +// 后端状态枚举统一映射为 Element Plus tag 的文案和类型。 const statusMeta = (status) => { const value = String(status || '').toUpperCase() if (value === 'SUCCESS') return { label: '执行完成', type: 'success' } @@ -84,6 +87,7 @@ const statusMeta = (status) => { return { label: '执行中', type: 'warning' } } +// 子流程整体的分析结论由内部判定型节点汇总得出,和执行成功/失败状态分开展示。 const analysisMeta = computed(() => { if (props.data.analysisStatus === 'PASSED') return { label: '分析通过', type: 'success' } if (props.data.analysisStatus === 'FAILED') return { label: '分析未通过', type: 'danger' } @@ -91,6 +95,7 @@ const analysisMeta = computed(() => { return null }) +// 后端以毫秒返回耗时,短耗时保留 ms,较长耗时转秒,避免标题区域过长。 const formatDuration = (value) => { const duration = Number(value) if (!Number.isFinite(duration) || duration < 0) return '-' @@ -98,6 +103,7 @@ const formatDuration = (value) => { return `${(duration / 1000).toFixed(2)} s` } +// JSON Viewer 只在确实有内容时展示,避免空对象/空数组占据节点详情空间。 const hasValue = (value) => { if (value === null || value === undefined || value === '') return false if (Array.isArray(value)) return value.length > 0 @@ -131,6 +137,7 @@ const hasValue = (value) => { min-width: 0; flex: 1; gap: 8px; + padding-right: 8px; } diff --git a/src/views/flow/components/TaskExecutionLog.vue b/src/views/flow/components/TaskExecutionLog.vue index c698d7d..31714e5 100644 --- a/src/views/flow/components/TaskExecutionLog.vue +++ b/src/views/flow/components/TaskExecutionLog.vue @@ -167,14 +167,20 @@ const parseObject = (value) => { } } +// 统一合并新版 paramsOut 和旧版 actualResult/extraInfo,后写入的 extraInfo 拥有最高展示优先级。 const outputOf = (log) => ({ ...parseObject(log?.paramsOut), ...parseObject(log?.actualResult), ...parseObject(log?.extraInfo), }) +// _executionContext 保存后端补充的运行上下文,例如循环层级、事件类型和任务内出现次数。 const executionContextOf = (log) => outputOf(log)._executionContext || {} +/** + * 从新旧字段中读取循环路径,并兼容后端可能返回的数组、数字或分隔字符串。 + * 返回空数组表示该日志不属于循环轮次。 + */ const rawIterationPathOf = (log) => { const values = [log?.iterations, executionContextOf(log).iterations, log?.iteration] for (const value of values) { @@ -188,9 +194,8 @@ const rawIterationPathOf = (log) => { return [] } -// Historical NODE_STARTED records were persisted before the loop path was copied -// into the event. Pair them with the next terminal event of the same node so old -// task logs can be folded without rewriting database rows. +// 历史 NODE_STARTED 记录入库时还没有写入循环路径,通过同一节点后续终态事件反推, +// 这样旧任务日志也能被折叠进对应轮次,而不需要修改数据库历史数据。 const inferredIterationPaths = computed(() => { const result = new WeakMap() const pendingStarts = new Map() @@ -215,19 +220,21 @@ const inferredIterationPaths = computed(() => { return result }) +// 优先使用日志自身的循环路径;没有时再使用历史日志的推断结果。 const iterationPathOf = (log) => { const path = rawIterationPathOf(log) return path.length ? path : inferredIterationPaths.value.get(log) || [] } +// 节点标识 + 循环路径 + 任务内出现次数共同决定一条开始/结束事件是否属于同一次执行。 const executionKeyOf = (log) => { const context = executionContextOf(log) const nodeKey = log?.nodeId || log?.executionStep || log?.nodeName || log?.logContent || '' return `${nodeKey}:${iterationPathOf(log).join('.')}:${context.itemOccurrence || ''}` } -// User-facing task logs keep an unmatched start event while a node is running, then -// replace it with the terminal event. Structural wrapper nodes do not add business value. +// 面向用户的日志在节点运行中保留开始事件,终态事件到达后替换开始事件; +// 开始、结束、循环和子流程这类结构节点没有业务信息,非失败场景下隐藏。 const visibleLogs = computed(() => { const source = Array.isArray(props.list) ? props.list : [] if (isDebugView.value) return source @@ -252,6 +259,9 @@ const logContentOf = (log) => log?.logContent || log?.message || [log?.action || log?.nodeType, log?.status].filter(Boolean).join(' - ') || '节点执行记录' +/** + * 提取循环折叠标题上的分析判定。非判定型日志返回 null,避免普通节点影响“分析通过”摘要。 + */ const analysisPassedOf = (log) => { const output = outputOf(log) const analysisType = String(output.analysisType || '').toUpperCase() @@ -273,6 +283,10 @@ const toggleIteration = (key) => { expandedIterations.value = next } +/** + * 根据同一循环轮次内的日志状态推导折叠标题状态。 + * 优先级为失败 > 正在执行 > 已完成,兼容数字状态和事件型上下文。 + */ const statusOfIteration = (logs) => { const statuses = logs.map(log => String(log?.status ?? log?.logStatus ?? '').toUpperCase()) const contexts = logs.map(log => String(executionContextOf(log).eventType || '').toUpperCase()) @@ -298,6 +312,10 @@ const statusOfIteration = (logs) => { return { label: '执行完成', type: 'success' } } +/** + * 把扁平日志列表转换成“循环标题 + 可展开子日志”的展示序列。 + * 这一步只改变展示结构,不修改 props.list 中的原始日志对象。 + */ const displayLogs = computed(() => { const source = visibleLogs.value const groups = new Map() @@ -340,6 +358,7 @@ const displayLogs = computed(() => { return result }) +// 调试模式展示未被分析面板消费的原始输出,方便排查节点执行结果。 const genericOutputOf = (log) => { if (analysisOf(log)) return '' const output = { ...outputOf(log) } @@ -349,6 +368,7 @@ const genericOutputOf = (log) => { return JSON.stringify(output, null, 2) } +// 用户模式只保留最有业务含义的一行摘要,避免把完整 JSON 暴露给非调试场景。 const businessSummaryOf = (log) => { if (analysisOf(log) || mediaOf(log).length) return '' const output = { ...outputOf(log) } diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue index 0cbc556..f4f5862 100644 --- a/src/views/flow/components/params/BasicNodeParams.vue +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -124,7 +124,6 @@ import { computed, ref, reactive, watch, nextTick } from 'vue' import { Plus, Minus } from '@element-plus/icons-vue' import FormItemRecursive from '../FormItemRecursive.vue' import RobotExecutionTarget from './RobotExecutionTarget.vue' -import { getInput } from '@/utils/flow' import { useQuote } from './useQuote.js' import { getArmStatus, getArmJointState, getAgvStatus, getAgvStations } from '@/api/flow/flow' import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot' diff --git a/src/views/flow/components/params/CodeNodeParams.vue b/src/views/flow/components/params/CodeNodeParams.vue index bdac619..0969920 100644 --- a/src/views/flow/components/params/CodeNodeParams.vue +++ b/src/views/flow/components/params/CodeNodeParams.vue @@ -198,7 +198,6 @@ import { ref, reactive, watch, nextTick, onBeforeUnmount, onMounted } from 'vue' import { Plus, Minus } from '@element-plus/icons-vue' import FormItemRecursive from '../FormItemRecursive.vue' import * as monaco from 'monaco-editor'; -import { getInput } from '@/utils/flow' import { useQuote } from './useQuote.js' const props = defineProps({ diff --git a/src/views/flow/components/params/DeviceUniversal.vue b/src/views/flow/components/params/DeviceUniversal.vue index b3f1b95..285c4d5 100644 --- a/src/views/flow/components/params/DeviceUniversal.vue +++ b/src/views/flow/components/params/DeviceUniversal.vue @@ -203,7 +203,6 @@ import { Plus, Minus } from '@element-plus/icons-vue' import FormItemRecursive from '../FormItemRecursive.vue' import RobotExecutionTarget from './RobotExecutionTarget.vue' import * as monaco from 'monaco-editor'; -import { getInput } from '@/utils/flow' import { useQuote } from './useQuote.js' import { ElMessage } from 'element-plus' diff --git a/src/views/flow/components/params/EndParams.vue b/src/views/flow/components/params/EndParams.vue index a80fdf7..dc0b971 100644 --- a/src/views/flow/components/params/EndParams.vue +++ b/src/views/flow/components/params/EndParams.vue @@ -48,10 +48,15 @@ const quoteOptions = ref([]) const clone = (value) => JSON.parse(JSON.stringify(value ?? null)) const key = () => `output_${Date.now()}_${Math.random().toString(16).slice(2)}` +// 结束节点的输出只能来自已经执行过的开始参数或上游节点输出,面板展开时实时刷新引用树。 const refreshQuoteOptions = (visible = true) => { if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || [] } +/** + * 根据级联引用路径找到源字段定义,用于继承类型、子字段和默认说明。 + * 路径格式为 [节点ID, input/output, 字段名, 子字段名...]。 + */ const findDefinition = (quote) => { if (!Array.isArray(quote) || quote.length < 3) return null const graph = globalThis.lf.getGraphData() @@ -67,6 +72,10 @@ const findDefinition = (quote) => { return definition } +/** + * 用户切换引用来源后同步输出契约的类型和层级结构。 + * 未手动填写名称/说明时,优先继承被引用字段的元信息。 + */ const changeQuote = (item, value) => { item.quoteType = value?.[1] || '' const definition = findDefinition(value) @@ -104,6 +113,10 @@ const init = () => { refreshQuoteOptions() } +/** + * 校验流程输出契约并写回结束节点。 + * nodeParams 保存运行时取值引用,outputParams 保存对外暴露的输出字段说明。 + */ const validateAndSave = () => { const names = outputs.value.map((item) => item.name.trim()) if (names.some((name) => !name)) { diff --git a/src/views/flow/components/params/HttpNodeParams.vue b/src/views/flow/components/params/HttpNodeParams.vue index b3417fb..5306dc3 100644 --- a/src/views/flow/components/params/HttpNodeParams.vue +++ b/src/views/flow/components/params/HttpNodeParams.vue @@ -339,7 +339,7 @@ import { ref, reactive, watch, nextTick, onMounted, onBeforeUnmount } from 'vue' import { Plus, Minus } from '@element-plus/icons-vue' import FormItemRecursive from '../FormItemRecursive.vue' import * as monaco from 'monaco-editor'; -import { getInput, transformHttpNodeData } from '@/utils/flow' +import { transformHttpNodeData } from '@/utils/flow' import { useQuote } from './useQuote.js' const props = defineProps({ diff --git a/src/views/flow/components/params/MediaAnalysisParams.vue b/src/views/flow/components/params/MediaAnalysisParams.vue index b53d4af..ed55a46 100644 --- a/src/views/flow/components/params/MediaAnalysisParams.vue +++ b/src/views/flow/components/params/MediaAnalysisParams.vue @@ -369,7 +369,7 @@ const normalizeStoredTuning = (value, preset) => { } } -// getInput 根据当前节点 ID 返回可引用的上游输出,只在级联面板打开时刷新以减少无效计算。 +// getUpstreamInput 根据当前节点 ID 返回可引用的上游输出,只在级联面板打开时刷新以减少无效计算。 const loadQuoteOptions = (visible) => { if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || [] } diff --git a/src/views/flow/components/params/SdAgentNodeParams.vue b/src/views/flow/components/params/SdAgentNodeParams.vue index 64a580d..87bdb20 100644 --- a/src/views/flow/components/params/SdAgentNodeParams.vue +++ b/src/views/flow/components/params/SdAgentNodeParams.vue @@ -142,7 +142,7 @@ import { ref, reactive, watch, nextTick } from 'vue' import { Plus, Minus } from '@element-plus/icons-vue' import FormItemRecursive from '../FormItemRecursive.vue' -import { getInput, transformSdAgentNodeData } from '@/utils/flow' +import { transformSdAgentNodeData } from '@/utils/flow' import { useQuote } from './useQuote.js' const props = defineProps({ diff --git a/src/views/flow/components/params/SubFlowParams.vue b/src/views/flow/components/params/SubFlowParams.vue index 65c60d8..5815ea2 100644 --- a/src/views/flow/components/params/SubFlowParams.vue +++ b/src/views/flow/components/params/SubFlowParams.vue @@ -110,10 +110,15 @@ const latestOption = computed(() => options.value.find((item) => item.itemId === const selectedOptionAvailable = computed(() => options.value.some((item) => item.versionId === selection.versionId)) const projectLabel = computed(() => selection.projectCode === 'AIMA' ? '爱玛' : '长安') +// 引用来源随画布连线和节点输出变化而变化,级联面板每次展开时重新读取最新上游字段。 const refreshQuoteOptions = (visible = true) => { if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || [] } +/** + * 从节点已有配置恢复子流程选择和输入/输出契约。 + * nodeParams 存储的是输入引用,outputParams 存储的是子流程对外暴露的输出定义。 + */ const restore = () => { const stored = props.data.properties?.subFlow || {} Object.assign(selection, { @@ -137,6 +142,10 @@ const restore = () => { refreshQuoteOptions() } +/** + * 应用一个已发布版本的契约。 + * 同名输入会沿用当前已经配置的引用,避免用户同步版本后重复选择所有上游字段。 + */ const applyOption = (option) => { if (!option) return const existing = new Map(inputs.value.map((item) => [item.name, item])) @@ -174,6 +183,10 @@ const loadOptions = async () => { } } +/** + * 把子流程需要的机器人角色和设备用途合并到当前流程开始节点。 + * 同 roleKey 的资源取更严格的 required/quantity,设备用途类型冲突时直接阻断保存。 + */ const mergeResourceRoles = () => { const graph = globalThis.lf.getGraphData() const start = graph.nodes.find((node) => node.type === 'start') @@ -211,6 +224,10 @@ const mergeResourceRoles = () => { return { graph, start, roles } } +/** + * 校验并写回子流程节点配置。 + * 保存前先在临时图上执行资源清理,确保切换子流程版本后不残留已不用的自动托管设备用途。 + */ const validateAndSave = () => { if (!selection.versionId) { ElMessage.warning('请选择已发布工作流') diff --git a/src/views/flow/components/params/useQuote.js b/src/views/flow/components/params/useQuote.js index 35c9861..269ff6a 100644 --- a/src/views/flow/components/params/useQuote.js +++ b/src/views/flow/components/params/useQuote.js @@ -5,13 +5,17 @@ export function useQuote(nodeId) { const quoteOptions = ref([]) const cascaderRefs = ref([]) + // 引用树必须按当前节点重新计算,避免选择到同级后置节点、下游节点或其他循环体内部字段。 const refreshQuoteOptions = () => { quoteOptions.value = getUpstreamInput(nodeId) || [] } - // The option tree must exist before Cascader can render a persisted path. + // Cascader 渲染已保存路径前需要先存在完整 option 树,否则回显会丢失末级字段。 refreshQuoteOptions() + /** + * 参数类型在手输和引用之间切换时,清空另一侧的值,避免保存出互相冲突的 input/quote。 + */ const handleTypeChange = (property) => { if (!property) return if (property.type === 'input') { @@ -22,12 +26,16 @@ export function useQuote(nodeId) { } } + /** + * 级联选择器只把路径写入表单项,quoteType 由路径第二级 input/output 推导。 + */ const cascaderChange = (value, property) => { if (!property) return property.quote = Array.isArray(value) ? [...value] : [] property.quoteType = value?.[1] === 'input' ? 'input' : 'output' } + // 面板打开时重新读取上游节点,承接刚刚新增的连线或输出字段。 const visibleChange = (visible) => { if (visible) refreshQuoteOptions() } diff --git a/src/views/flow/config.js b/src/views/flow/config.js index cb7451c..d3105d1 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -182,6 +182,7 @@ export const collapseList = [ action: 'SUB_FLOW', nodeType: 'NONE', desc: "执行同一项目中已发布的工作流", + // 具体输入/输出契约在参数面板选择发布版本后同步,配置清单只提供初始空壳。 subFlow: {}, nodeParams: [], outputParams: [], diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue index 441b33d..db60c9b 100644 --- a/src/views/flow/index.vue +++ b/src/views/flow/index.vue @@ -836,6 +836,7 @@ const versionLoading = ref(false); const versionList = ref([]); const versionPreviewMode = ref(false); const previewingVersionNo = ref(null); +// 试运行前需要先自动保存草稿,单独的准备态用于避免重复点击触发多次保存/弹窗。 const trialPreparing = ref(false); let draftBeforeVersionPreview = null; let oldFlowDataBeforeVersionPreview = null; @@ -890,6 +891,7 @@ const testRun = async () => { return; } + // 试运行使用后端持久化的草稿数据,先保存能保证画布上的最新变更参与本次执行。 trialPreparing.value = true; try { const saved = await saveDraft({ successMessage: "已自动保存当前草稿" }); @@ -1077,6 +1079,11 @@ const changeState = (value) => { loopFlowView(value.instId); }; +/** + * 从后端运行日志中的内部节点 ID 反推出所属子流程节点。 + * 后端存在新旧两类 ID 协议,这里统一归并到外层子流程节点,便于节点状态面板展示内部执行详情。 + * @param {string} runtimeNodeId 运行日志中的节点 ID + */ const resolveSubFlowParentNodeId = (runtimeNodeId) => { const markerIndexes = [runtimeNodeId.indexOf("__sf__"), runtimeNodeId.indexOf("__subflow__")] .filter((index) => index > 0); @@ -1093,6 +1100,10 @@ const resolveSubFlowParentNodeId = (runtimeNodeId) => { return parentNode?.id || null; }; +/** + * 运行日志中的入参/出参可能是 JSON 字符串,也可能已经是对象;统一转成对象供结果面板消费。 + * @param {*} value 运行日志字段值 + */ const parseRuntimeValue = (value) => { if (!value) return {}; if (typeof value === "object") return value; @@ -1104,6 +1115,10 @@ const parseRuntimeValue = (value) => { } }; +/** + * 兼容数组、单值和分隔字符串三种循环层级格式,返回可用于分组展示的循环路径。 + * @param {*} item 运行日志条目 + */ const runtimeIterationPath = (item) => { const value = item?.iterations ?? item?.iteration; if (Array.isArray(value)) return value.filter(part => part !== null && part !== ""); @@ -1111,6 +1126,11 @@ const runtimeIterationPath = (item) => { return String(value).split(/[-.,/]/).map(part => part.trim()).filter(Boolean); }; +/** + * 把同一节点在不同循环次数里的日志汇总成 iterationHistory,写回对应节点状态弹窗。 + * 这样单个子节点既保留最新状态,也能展开查看每次循环的输入、输出和错误。 + * @param {Array} logs 本次轮询返回的运行日志 + */ const emitLoopIterationOutputs = (logs) => { const grouped = new Map(); logs.forEach((log) => { @@ -1150,6 +1170,10 @@ const emitLoopIterationOutputs = (logs) => { }); }; +/** + * 将后端可能返回的布尔字符串转为三态结果;null 表示无法判断,避免误报“未通过”。 + * @param {*} value 待转换值 + */ const runtimeBoolean = (value) => { if (value === true || value === false) return value; if (String(value).toLowerCase() === "true") return true; @@ -1157,6 +1181,11 @@ const runtimeBoolean = (value) => { return null; }; +/** + * 识别需要参与通过/未通过汇总的音视频分析节点,并提取业务判定结果。 + * @param {*} log 运行日志条目 + * @param {*} output 已解析的节点输出 + */ const decisionAnalysisOf = (log, output) => { const action = String(log?.action || "").toUpperCase(); const analysisType = String(output?.analysisType || "").toUpperCase(); @@ -1169,6 +1198,7 @@ const decisionAnalysisOf = (log, output) => { }; }; +// 子流程内部日志只携带动作枚举时,使用这张表转成更适合弹窗展示的中文名称。 const SUB_FLOW_ACTION_LABELS = Object.freeze({ START: "开始", END: "结束", @@ -1193,6 +1223,10 @@ const SUB_FLOW_ACTION_LABELS = Object.freeze({ const subFlowActionLabel = (log) => SUB_FLOW_ACTION_LABELS[String(log?.action || "").toUpperCase()] || String(log?.action || log?.nodeType || "内部节点"); +/** + * 优先从日志消息中提取用户配置的节点名;取不到时再退回到动作枚举文案。 + * @param {*} log 子流程内部运行日志 + */ const subFlowNodeName = (log) => { const action = String(log?.action || ""); const match = String(log?.message || "").match(/^\[([^\]]+)]\s*执行/); @@ -1201,6 +1235,11 @@ const subFlowNodeName = (log) => { return subFlowActionLabel(log); }; +/** + * 将子流程内部节点日志聚合到外层子流程节点,生成分组后的执行明细和整体分析结论。 + * 子流程可能包含循环,因此内部日志按循环路径拆分;普通步骤统一放到 root 分组。 + * @param {Array} logs 本次轮询返回的运行日志 + */ const emitSubFlowContainerOutputs = (logs) => { const subFlowNodes = (lf.getGraphData().nodes || []).filter(node => node.type === "subFlow"); subFlowNodes.forEach((subFlowNode) => { @@ -1212,6 +1251,7 @@ const emitSubFlowContainerOutputs = (logs) => { const grouped = new Map(); internalLogs.forEach((log) => { const nodeType = String(log?.nodeType || "").toUpperCase(); + // 开始/结束这类结构节点没有业务输出,成功时隐藏;失败时保留,避免吞掉异常信息。 if (["START", "END", "SUB_START", "SUB_END"].includes(nodeType) && String(log?.status || "").toUpperCase() !== "FAILED") return; const iteration = runtimeIterationPath(log); @@ -1252,6 +1292,7 @@ const emitSubFlowContainerOutputs = (logs) => { const allNodes = executions.flatMap(execution => execution.nodes); const analyses = allNodes.filter(node => node.isAnalysis); + // 有任一分析节点失败即认为子流程分析未通过;仍在执行或未返回判定时保持 RUNNING。 const analysisStatus = analyses.some(node => node.passed === false || String(node.status || "").toUpperCase() === "FAILED") ? "FAILED" : analyses.some(node => node.passed === null || String(node.status || "").toUpperCase() === "RUNNING") @@ -1296,6 +1337,11 @@ const emitSubFlowContainerOutputs = (logs) => { }); }; +/** + * 汇总循环容器下所有子节点的分次执行结果,供循环节点自身的状态弹窗展示。 + * 同时计算每次循环的执行状态和分析判定,便于用户快速定位失败的循环轮次。 + * @param {Array} logs 本次轮询返回的运行日志 + */ const emitLoopContainerOutputs = (logs) => { const graph = lf.getGraphData(); const graphNodes = graph.nodes || []; @@ -1308,6 +1354,7 @@ const emitLoopContainerOutputs = (logs) => { .filter(node => String(node.properties?.parentId || "") === String(loopNode.id)) .map(node => node.id), ].map(String)); + // 如果循环体包含判定型分析节点,即使当前轮次尚未返回 passed,也需要显示“分析中”。 const loopHasDecisionAnalysis = [...childIds].some((childId) => { const action = String(nodeById.get(childId)?.properties?.action || "").toUpperCase(); return ["AUDIO_EVENT_CLASSIFY", "VIDEO_ANALYZE"].includes(action); diff --git a/src/views/flow/nodes/function/subFlow.vue b/src/views/flow/nodes/function/subFlow.vue index 546c8f8..758083d 100644 --- a/src/views/flow/nodes/function/subFlow.vue +++ b/src/views/flow/nodes/function/subFlow.vue @@ -43,6 +43,7 @@ const runtimes = ref(0) const inputJsonData = ref({}) const outputJsonData = ref({}) const errorInfoData = ref('') +// 优先使用主页面轮询聚合出的 subFlowExecution;没有内部日志时退回到普通输出,保证旧数据仍可展示。 const subFlowExecution = computed(() => outputJsonData.value?.subFlowExecution || { name: props.properties?.subFlow?.detectName || props.properties?.name, versionNo: props.properties?.subFlow?.versionNo, @@ -50,6 +51,7 @@ const subFlowExecution = computed(() => outputJsonData.value?.subFlowExecution | executions: [], }) +// 子流程输出来自后端运行日志,兼容 JSON 字符串和对象;解析失败时保留原值用于排查。 const parseValue = (value) => { if (!value) return {} if (typeof value === 'object') return value @@ -65,6 +67,7 @@ const setNodeName = (name) => { onMounted(() => { onFlowEvent('changeNodeState', (data) => { if (data.nodeId !== props.model.id) return + // 外层子流程节点状态由主页面统一聚合内部日志后派发,这里只负责同步到节点 UI。 nodeOperatingStatus.value = data.status runtimes.value = data.endTime && data.startTime ? data.endTime - data.startTime : 0 inputJsonData.value = parseValue(data.paramsIn) diff --git a/src/views/flow/vue-flow/FlowNode.vue b/src/views/flow/vue-flow/FlowNode.vue index a44b597..b08f335 100644 --- a/src/views/flow/vue-flow/FlowNode.vue +++ b/src/views/flow/vue-flow/FlowNode.vue @@ -94,6 +94,7 @@ const props = defineProps({ connectable: [Boolean, Number, String, Function], }); +// 画布节点类型到 Vue 组件的唯一映射表;新增节点类型时需要在这里接入渲染组件。 const componentMap = { start: StartNode, end: EndNode,