feat: 增强流资源管理和节点执行组件

- 增加了检测流资源中引用循环的功能,以防止循环依赖。
- 改进了流引用的验证,以确保节点输出中不存在循环依赖。
- 增强了资源定义清理功能,以便在执行前移除过时的设备ID。
- 已实现复制资源角色的合并,以优化现有资源的优先级并避免重复。
- 更新了LoopIterationResults组件,以管理运行中迭代的自动扩展。
- 增强了NodeState和NodeTitle组件,以支持循环和子流结果的更宽弹出框显示。
- 优化了ParamsDrawer和SubFlowExecutionResults组件,以处理动态参数更新和执行结果。
- 改进了TaskExecutionLog,以统一输出处理并增强用户对日志的可见性。
- 更新了各种参数组件,以简化输入处理并提升用户体验。
- 在代码中添加了全面的注释和文档,以提高可维护性和易理解性。
This commit is contained in:
zhanghao 2026-08-21 15:01:16 +08:00
parent 657b7b9338
commit d733ea8525
22 changed files with 389 additions and 23 deletions

View File

@ -1,6 +1,15 @@
import request from '@/utils/request' import request from '@/utils/request'
// 发布流程 /**
* 文件说明:流程设计器相关接口封装。
* 约定:本文件只负责描述请求地址、请求方式和入参承载位置,不在这里处理业务返回结构。
*/
/**
* 发布流程。
* @param {Object} data 发布请求体,通常包含流程 ID、流程 JSON、发布说明等后端要求字段。
* @returns {Promise} 后端发布结果。
*/
export function flowDeploy(data) { export function flowDeploy(data) {
return request({ return request({
url: '/flow/publish', url: '/flow/publish',
@ -9,18 +18,40 @@ export function flowDeploy(data) {
}) })
} }
/**
* 保存流程草稿。
* 试运行前会先保存草稿,确保后端执行的是当前画布最新内容。
* @param {Object} data 草稿数据,包含流程 ID、flowData、draftRevision 等。
* @returns {Promise} 草稿保存结果,成功时后端会返回新的草稿版本号。
*/
export function flowSaveDraft(data) { export function flowSaveDraft(data) {
return request({ url: '/flow/draft', method: 'put', data }) return request({ url: '/flow/draft', method: 'put', data })
} }
/**
* 查询某个流程的发布版本列表。
* @param {string|number} itemId 流程定义 ID。
* @returns {Promise} 发布记录列表。
*/
export function flowVersionList(itemId) { export function flowVersionList(itemId) {
return request({ url: `/flow/versions/${itemId}`, method: 'get' }) return request({ url: `/flow/versions/${itemId}`, method: 'get' })
} }
/**
* 查询指定发布版本的完整详情。
* @param {string|number} versionId 发布版本 ID。
* @returns {Promise} 版本详情,通常包含版本号、发布说明和对应 flowData。
*/
export function flowVersionDetail(versionId) { export function flowVersionDetail(versionId) {
return request({ url: `/flow/version/${versionId}`, method: 'get' }) return request({ url: `/flow/version/${versionId}`, method: 'get' })
} }
/**
* 查询可作为子流程引用的已发布工作流。
* currentItemId 用于后端过滤当前流程自身,以及会形成循环引用链路的工作流。
* @param {string|number} currentItemId 当前正在编辑的流程 ID。
* @returns {Promise} 可选子流程发布版本列表。
*/
export function flowSubFlowOptions(currentItemId) { export function flowSubFlowOptions(currentItemId) {
return request({ return request({
url: '/flow/subflows', 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) { export function flowRestoreDraft(versionId, data) {
return request({ url: `/flow/version/${versionId}/restore-draft`, method: 'post', 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) { export function flowRollbackVersion(versionId, data) {
return request({ url: `/flow/version/${versionId}/rollback`, method: 'post', data }) return request({ url: `/flow/version/${versionId}/rollback`, method: 'post', data })
} }
/**
* 正式执行流程。
* @param {Object} data 执行请求体,通常包含流程 ID、任务参数、资源分配等。
* @returns {Promise} 执行启动结果。
*/
export function flowExecute(data) { export function flowExecute(data) {
return request({ return request({
url: '/flow/execute', url: '/flow/execute',
@ -45,6 +93,11 @@ export function flowExecute(data) {
}) })
} }
/**
* 执行流程运行过程中的控制或单节点动作。
* @param {Object} data 动作请求体,具体字段由后端按 action 类型解析。
* @returns {Promise} 动作执行结果。
*/
export function flowAction(data) { export function flowAction(data) {
return request({ return request({
url: '/flow/action', url: '/flow/action',
@ -53,6 +106,11 @@ export function flowAction(data) {
}) })
} }
/**
* 试运行流程。
* @param {Object} data 试运行请求体,通常包含流程 ID、草稿版本、运行参数和资源选择。
* @returns {Promise} 试运行实例创建结果。
*/
export function flowExecuteTrial(data) { export function flowExecuteTrial(data) {
return request({ return request({
url: '/flow/executeTrial', url: '/flow/executeTrial',
@ -61,6 +119,11 @@ export function flowExecuteTrial(data) {
}) })
} }
/**
* 查询流程实例执行日志或节点状态快照。
* @param {Object} data 查询条件,常用字段包括 itemId、instId、taskId。
* @returns {Promise} 节点运行日志列表。
*/
export function flowView(data) { export function flowView(data) {
return request({ return request({
url: '/flow/view', url: '/flow/view',
@ -69,6 +132,11 @@ export function flowView(data) {
}) })
} }
/**
* 暂停正在运行的流程实例。
* @param {string|number} instId 流程实例 ID。
* @returns {Promise} 暂停结果。
*/
export function flowPause(instId) { export function flowPause(instId) {
return request({ return request({
url: `/flow/pause/${instId}`, url: `/flow/pause/${instId}`,
@ -76,6 +144,11 @@ export function flowPause(instId) {
}) })
} }
/**
* 恢复已暂停的流程实例。
* @param {string|number} instId 流程实例 ID。
* @returns {Promise} 恢复结果。
*/
export function flowResume(instId) { export function flowResume(instId) {
return request({ return request({
url: `/flow/resume/${instId}`, url: `/flow/resume/${instId}`,
@ -83,6 +156,11 @@ export function flowResume(instId) {
}) })
} }
/**
* 终止正在运行或暂停中的流程实例。
* @param {string|number} instId 流程实例 ID。
* @returns {Promise} 终止结果。
*/
export function flowStop(instId) { export function flowStop(instId) {
return request({ return request({
url: `/flow/stop/${instId}`, url: `/flow/stop/${instId}`,
@ -90,6 +168,11 @@ export function flowStop(instId) {
}) })
} }
/**
* 查询机械臂当前位姿。
* @param {Object} params 查询参数,通常包含机器人或设备标识。
* @returns {Promise} 机械臂位姿信息。
*/
export function getArmStatus(params) { export function getArmStatus(params) {
return request({ return request({
url: '/api/arm/getPose', url: '/api/arm/getPose',
@ -98,6 +181,11 @@ export function getArmStatus(params) {
}) })
} }
/**
* 查询 AGV 底盘运行状态。
* @param {Object} params 查询参数,通常包含机器人或设备标识。
* @returns {Promise} AGV 运行状态。
*/
export function getAgvStatus(params) { export function getAgvStatus(params) {
return request({ return request({
url: '/api/agv/getRuntimeState', url: '/api/agv/getRuntimeState',
@ -106,6 +194,11 @@ export function getAgvStatus(params) {
}) })
} }
/**
* 查询 AGV 可用站点列表。
* @param {Object} params 查询参数,通常包含机器人或地图/设备信息。
* @returns {Promise} 站点列表。
*/
export function getAgvStations(params) { export function getAgvStations(params) {
return request({ return request({
url: '/api/agv/listStations', url: '/api/agv/listStations',
@ -114,6 +207,11 @@ export function getAgvStations(params) {
}) })
} }
/**
* 查询机械臂关节状态。
* @param {Object} params 查询参数,通常包含机器人或设备标识。
* @returns {Promise} 机械臂关节角度/状态信息。
*/
export function getArmJointState(params) { export function getArmJointState(params) {
return request({ return request({
url: '/api/arm/getJointState', url: '/api/arm/getJointState',

View File

@ -1,3 +1,15 @@
/**
* 文件说明:流程设计器通用工具函数。
* 该文件大量依赖画布全局实例 lf,用于读取/修改当前流程图、生成参数引用树和转换节点表单数据。
*/
/**
* 生成当前画布所有可展示节点的输入/输出字段树。
* 该函数只负责构造完整候选树,不判断节点是否位于当前节点上游;需要上游限制时使用 getUpstreamInput。
* @param {string} nodeId 当前节点 ID,候选列表会排除该节点自身。
* @param {boolean} isStrict 历史保留参数,旧逻辑曾用于要求节点必须存在入边;当前未启用。
* @returns {Array} Element Plus Cascader 可使用的节点参数树。
*/
export const getInput = (nodeId, isStrict = true) => { export const getInput = (nodeId, isStrict = true) => {
const data = lf.getGraphData(); const data = lf.getGraphData();
// let flag = false // let flag = false
@ -13,6 +25,7 @@ export const getInput = (nodeId, isStrict = true) => {
// if (flag) { // if (flag) {
const arr = [] const arr = []
// 结构节点、控制节点和终止节点不产生可供业务节点引用的有效输入/输出。
const filterNodes = ['sleep', 'stopLoop', 'subEnd', 'subStart', 'branch', 'end'] const filterNodes = ['sleep', 'stopLoop', 'subEnd', 'subStart', 'branch', 'end']
data.nodes.forEach(item => { data.nodes.forEach(item => {
if (item.id === nodeId) { if (item.id === nodeId) {
@ -23,6 +36,7 @@ export const getInput = (nodeId, isStrict = true) => {
return return
} }
// 每个节点下固定分为“输入”和“输出”两组,叶子节点由 transformTree 递归补齐。
const obj = { const obj = {
value: item.id, value: item.id,
label: item.properties.name, label: item.properties.name,
@ -55,6 +69,13 @@ export const getInput = (nodeId, isStrict = true) => {
// return flag // return flag
} }
/**
* 将参数定义数组递归转换为级联选择器节点。
* @param {Array} arr 参数定义数组,支持 children 嵌套。
* @param {Array} parent 当前层级要写入的级联节点数组。
* @param {"input"|"output"} type 当前参数所属分组,用于后续保存 quoteType。
* @returns {Array} 转换后的 parent 数组。
*/
const transformTree = (arr, parent = [], type) => { const transformTree = (arr, parent = [], type) => {
arr.forEach(item => { arr.forEach(item => {
const currentNode = { const currentNode = {
@ -74,6 +95,10 @@ const transformTree = (arr, parent = [], type) => {
return parent; return parent;
}; };
/**
* 读取开始节点的输入参数,并补充运行表单需要的默认值和 propPath。
* @returns {Array} 已带 value/propPath 的开始参数数组。
*/
export const getStartNodeFormData = () => { export const getStartNodeFormData = () => {
const flowData = lf.getGraphData(); const flowData = lf.getGraphData();
const startNode = flowData.nodes.find(item => item.type === 'start') const startNode = flowData.nodes.find(item => item.type === 'start')
@ -81,6 +106,12 @@ export const getStartNodeFormData = () => {
return data return data
} }
/**
* 给参数定义递归补充默认值 value 和表单校验路径 propPath。
* @param {Object|Array} data 参数定义或参数定义数组。
* @param {string} schemeId 特殊参数 schemeId 的默认值。
* @returns {Object|Array} 原结构附加 value/propPath 后的结果。
*/
export const addValueProperty = (data, schemeId = '') => { export const addValueProperty = (data, schemeId = '') => {
// 处理单个对象或对象数组 // 处理单个对象或对象数组
const processItem = (item, index, path) => { 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, '') 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 = []) => { export const recursiveFilter = (data, id, type = ['loop', 'customGroup'], result = []) => {
data.forEach((item) => { data.forEach((item) => {
if (type.includes(item.type) && item.children && item.children.includes(id)) { 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 return result
} }
/**
* 将树形表单字段转换为普通对象。
* 叶子字段取 value;存在 children 的字段会递归生成嵌套对象。
* @param {Array} arr 表单字段数组。
* @param {Object} data 写入目标对象,递归时复用。
* @returns {Object} 转换后的对象。
*/
export const formatTableData = (arr, data = {}) => { export const formatTableData = (arr, data = {}) => {
arr.forEach(item => { arr.forEach(item => {
data[item.name] = item.value data[item.name] = item.value
@ -126,9 +173,14 @@ export const formatTableData = (arr, data = {}) => {
return data return data
} }
/**
* 重新创建与指定节点相连的边。
* 节点宽度或锚点位置变化后,删除再添加边可以触发画布重新计算连线位置。
* @param {string} nodeId 需要刷新连线的节点 ID。
*/
export const addNewEdge = (nodeId) => { export const addNewEdge = (nodeId) => {
const { edges } = lf.getGraphData(); const { edges } = lf.getGraphData();
const ttt = edges.slice(); // 浅拷贝(比 JSON 深拷贝高效 10x+) const ttt = edges.slice(); // 浅拷贝即可保护遍历集合,避免删除边时影响原数组迭代。
const edgesToDelete = ttt.filter(_edge => const edgesToDelete = ttt.filter(_edge =>
_edge.sourceNodeId === nodeId || _edge.targetNodeId === nodeId _edge.sourceNodeId === nodeId || _edge.targetNodeId === nodeId
@ -140,6 +192,7 @@ export const addNewEdge = (nodeId) => {
clearTimeout(window.addNewEdgeTimer); // 清理上一次未执行的定时器 clearTimeout(window.addNewEdgeTimer); // 清理上一次未执行的定时器
} }
// 延迟到 DOM 和节点尺寸更新后再添加边,避免仍使用旧锚点位置。
window.addNewEdgeTimer = setTimeout(() => { window.addNewEdgeTimer = setTimeout(() => {
edgesToDelete.forEach(item => { edgesToDelete.forEach(item => {
lf.addEdge({ lf.addEdge({
@ -154,6 +207,11 @@ export const addNewEdge = (nodeId) => {
}, 50) }, 50)
} }
/**
* 立即刷新指定节点关联的边。
* 与 addNewEdge 不同,这里不做防抖和延迟,适合无需等待 DOM 尺寸变化的场景。
* @param {string} nodeId 需要刷新连线的节点 ID。
*/
export const newEdge = (nodeId) => { export const newEdge = (nodeId) => {
const { edges } = lf.getGraphData(); const { edges } = lf.getGraphData();
const arr = JSON.parse(JSON.stringify(edges)) 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) => { export const initNodeZoom = (modelId, nodeZoom, className, init=false) => {
const nodes = document.getElementsByClassName(modelId) const nodes = document.getElementsByClassName(modelId)
if (nodes.length > 0) { 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) => { export const getInputNumber = (nodeId) => {
const data = lf.getGraphData(); const data = lf.getGraphData();
let flag = false let flag = false
@ -212,6 +282,12 @@ export const getInputNumber = (nodeId) => {
return flag return flag
} }
/**
* 删除分支节点某个出口锚点上的所有连线。
* 删除条件分支时调用,避免已删除条件对应的边继续残留在画布上。
* @param {string} nodeId 分支节点 ID。
* @param {string} sourceAnchorId 分支出口锚点 ID。
*/
export const removeSwitchEdge = (nodeId, sourceAnchorId) => { export const removeSwitchEdge = (nodeId, sourceAnchorId) => {
const { edges } = lf.getGraphData(); const { edges } = lf.getGraphData();
const matchedEdges = edges.filter(item => { const matchedEdges = edges.filter(item => {
@ -221,8 +297,15 @@ export const removeSwitchEdge = (nodeId, sourceAnchorId) => {
matchedEdges.forEach(edge => lf.deleteEdge(edge.id)) matchedEdges.forEach(edge => lf.deleteEdge(edge.id))
} }
/**
* 构建“子节点 ID -> 父容器 ID”的映射。
* 统一兼容 VueFlow 当前 children 结构和旧流程保存的 properties.parentId 字段。
* @param {Object} data 流程图数据,包含 nodes/edges。
* @returns {Map<string, string>} 子节点到父节点的映射。
*/
const buildParentByChild = (data) => { const buildParentByChild = (data) => {
const result = new Map(); const result = new Map();
// 兼容 VueFlow children 集合和旧数据中的 properties.parentId,两种来源都可标识容器归属。
;(data.nodes || []).forEach((node) => { ;(data.nodes || []).forEach((node) => {
;(node.children || []).forEach((childId) => result.set(String(childId), String(node.id))); ;(node.children || []).forEach((childId) => result.set(String(childId), String(node.id)));
}); });
@ -232,6 +315,13 @@ const buildParentByChild = (data) => {
return result; return result;
}; };
/**
* 沿着有向边反向收集 targetId 的所有前置节点。
* result 由调用方传入,便于在不同容器层级中重复累加可见上游节点。
* @param {Object} data 流程图数据,包含 nodes/edges。
* @param {string} targetId 目标节点 ID。
* @param {Set<string>} result 前置节点 ID 集合。
*/
const collectPredecessors = (data, targetId, result) => { const collectPredecessors = (data, targetId, result) => {
const pending = [String(targetId)]; const pending = [String(targetId)];
while (pending.length) { 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<string>} 目标节点可引用的上游节点 ID 集合。
*/ */
export const getUpstreamNodeIds = (data, nodeId) => { export const getUpstreamNodeIds = (data, nodeId) => {
const graph = data || { nodes: [], edges: [] }; 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)) (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 currentTargetId = targetId;
let currentScopeId = parentByChild.get(targetId) || ''; let currentScopeId = parentByChild.get(targetId) || '';
const visitedScopes = new Set(); const visitedScopes = new Set();
@ -281,6 +373,12 @@ export const getUpstreamNodeIds = (data, nodeId) => {
return result; return result;
}; };
/**
* 生成级联选择器使用的上游入参/出参树。
* 开始节点只暴露输入参数,普通节点只暴露输出参数,防止用户选到运行时不可用的字段。
* @param {string} nodeId 当前节点 ID。
* @returns {Array} 已按可执行顺序过滤后的级联选项树。
*/
export const getUpstreamInput = (nodeId) => { export const getUpstreamInput = (nodeId) => {
const data = lf.getGraphData(); const data = lf.getGraphData();
const startIds = new Set( const startIds = new Set(
@ -298,6 +396,12 @@ export const getUpstreamInput = (nodeId) => {
.filter((option) => option.children.some((group) => group.children?.length)); .filter((option) => option.children.some((group) => group.children?.length));
} }
/**
* 将扁平节点数组按 children 字段转换成树结构。
* 节点关系弹窗使用该结果展示父子层级,例如循环容器内的子节点。
* @param {Array} data 流程节点数组。
* @returns {Array} 根节点数组,每个根节点递归带 children。
*/
export const convertToTree = (data) => { export const convertToTree = (data) => {
// 创建 id 到节点的映射 // 创建 id 到节点的映射
const nodeMap = new Map(); const nodeMap = new Map();
@ -341,6 +445,12 @@ export const convertToTree = (data) => {
} }
/**
* 递归过滤 name 为空的字段定义。
* 常用于保存输出参数前移除未填写名称的空行,避免生成不可引用的空字段。
* @param {*} obj 字段定义、字段数组或基础类型。
* @returns {*} 过滤后的结构;被剔除的对象返回 null。
*/
export const filterEmptyName = (obj) => { export const filterEmptyName = (obj) => {
// 1. 如果是数组,遍历每一项递归处理 // 1. 如果是数组,遍历每一项递归处理
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
@ -372,9 +482,10 @@ export const filterEmptyName = (obj) => {
} }
/** /**
* 将原始表单配置数组 转换为 目标对象格式 * 将 HTTP 节点面板中的表单数组转换为后端执行协议。
* @param {Array} source - 原始数据数组 * config/headers/params 保留 children 数组;body 会展开为 { type, json, formData }。
* @returns {Object} 转换后的目标格式对象 * @param {Array} source HTTP 节点原始表单配置数组。
* @returns {Object} 后端执行 HTTP 请求所需的结构化配置。
*/ */
export const transformHttpNodeData = (source) => { export const transformHttpNodeData = (source) => {
// 初始化结果对象 // 初始化结果对象
@ -420,6 +531,12 @@ export const transformHttpNodeData = (source) => {
return result; return result;
} }
/**
* 将智能体节点表单数组转换为后端执行协议。
* 普通配置字段保留 children;invokeTts 是开关值,需要从 item.input 提取为顶层字段。
* @param {Array} source 智能体节点原始表单配置数组。
* @returns {Object} 智能体调用配置。
*/
export const transformSdAgentNodeData = (source) => { export const transformSdAgentNodeData = (source) => {
// 初始化结果对象 // 初始化结果对象
const result = {}; const result = {};
@ -444,6 +561,12 @@ export const transformSdAgentNodeData = (source) => {
return result; return result;
} }
/**
* 将识别节点表单转换为后端协议。
* 普通字段写入 nodeParams;alarmRules 单独提取为告警规则数组,并兼容旧版 children 结构。
* @param {Array} source 识别节点原始表单配置数组。
* @returns {{nodeParams: Array, alarmRules: Array}} 识别节点配置。
*/
export const transformRecognizeNodeData = (source) => { export const transformRecognizeNodeData = (source) => {
// 初始化结果对象 // 初始化结果对象
const result = { const result = {

View File

@ -342,6 +342,10 @@ const collectActiveQuotes = (value, result = []) => {
return result return result
} }
/**
* 对节点引用依赖做深度优先遍历,发现 A 引用 B、B 又回到 A 的循环链路。
* states: 1 表示当前递归栈中,2 表示已确认无环;命中 1 时即可截取循环路径。
*/
const findReferenceCycle = (dependencies) => { const findReferenceCycle = (dependencies) => {
const states = new Map() const states = new Map()
const stack = [] const stack = []
@ -369,7 +373,10 @@ const findReferenceCycle = (dependencies) => {
return null return null
} }
/** Validates persisted references as well as choices made through the parameter panels. */ /**
* 校验已保存的引用配置和参数面板刚选择的引用配置。
* 当前只阻断循环引用,避免节点输出依赖后续又间接依赖自身,导致运行端无法拓扑执行。
*/
export const validateFlowReferences = (graph) => { export const validateFlowReferences = (graph) => {
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [] const nodes = Array.isArray(graph?.nodes) ? graph.nodes : []
const nodeById = new Map(nodes.map((node) => [String(node.id), node])) const nodeById = new Map(nodes.map((node) => [String(node.id), node]))
@ -395,6 +402,10 @@ export const validateFlowReferences = (graph) => {
return [] return []
} }
/**
* 保存或发布前移除旧版物理机器人/设备 ID,只保留资源角色和设备用途定义。
* 运行时会重新选择实际设备,避免把历史测试设备固化进流程模板。
*/
export const sanitizeFlowResourceDefinition = (sourceGraph) => { export const sanitizeFlowResourceDefinition = (sourceGraph) => {
const graph = normalizeFlowResourceSchema(sourceGraph) const graph = normalizeFlowResourceSchema(sourceGraph)
const start = getStartNode(graph) const start = getStartNode(graph)
@ -408,6 +419,10 @@ export const sanitizeFlowResourceDefinition = (sourceGraph) => {
return graph return graph
} }
/**
* 粘贴跨流程节点时合并其依赖的机器人角色和设备用途。
* 优先复用同标识或同名资源;找不到时才追加由流程节点托管的资源定义。
*/
export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNodes = []) => { export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNodes = []) => {
const graph = normalizeFlowResourceSchema(targetGraph) const graph = normalizeFlowResourceSchema(targetGraph)
const start = getStartNode(graph) const start = getStartNode(graph)
@ -437,6 +452,7 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo
targetRole ||= targetRoles[0] targetRole ||= targetRoles[0]
target.roleKey = targetRole.roleKey target.roleKey = targetRole.roleKey
// 设备用途可能只存在于来源流程,粘贴后需要映射到目标流程中可用的 slotKey。
const remapSlot = (slotKey) => { const remapSlot = (slotKey) => {
if (!slotKey || targetRole.deviceSlots?.some((slot) => slot.slotKey === slotKey)) return slotKey if (!slotKey || targetRole.deviceSlots?.some((slot) => slot.slotKey === slotKey)) return slotKey
const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === slotKey) const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === slotKey)
@ -459,6 +475,7 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo
[binding.parameterName, binding.slotKey] [binding.parameterName, binding.slotKey]
))) )))
const queueStepsParam = node.properties?.nodeParams?.find((param) => param.name === 'steps') const queueStepsParam = node.properties?.nodeParams?.find((param) => param.name === 'steps')
// 动作队列步骤内保存的是设备绑定名,合并资源后同步回真实 slotKey,单步执行才能定位设备。
if (Array.isArray(queueStepsParam?.input)) { if (Array.isArray(queueStepsParam?.input)) {
queueStepsParam.input.forEach((step) => { queueStepsParam.input.forEach((step) => {
if (bindingSlotByName.has(step?.deviceBindingKey)) { 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 || '任意设备' export const getDeviceKindName = (deviceKind) => DEVICE_KIND_META[Number(deviceKind)]?.name || '任意设备'
/**
* 汇总主流程和子流程的资源契约,生成运行前需要一次性分配的角色集合。
* 同一 roleKey 下的必选项取更严格配置,设备用途按 slotKey 合并。
*/
export const mergeResourceRoleDefinitions = (roleGroups = []) => { export const mergeResourceRoleDefinitions = (roleGroups = []) => {
const merged = [] const merged = []
roleGroups.flat().forEach((sourceRole) => { roleGroups.flat().forEach((sourceRole) => {

View File

@ -61,6 +61,8 @@ const props = defineProps({
const expandedIterations = ref([]) const expandedIterations = ref([])
let initialized = false let initialized = false
// 首次拿到运行结果时只自动展开一次:优先展开正在执行的轮次,否则展开最新一轮。
// 之后用户手动折叠/展开时不再被轮询数据刷新打断。
watch(() => props.history, (history) => { watch(() => props.history, (history) => {
if (initialized || !history?.length) return if (initialized || !history?.length) return
const active = history.find(item => String(item.status || '').toUpperCase() === 'RUNNING') const active = history.find(item => String(item.status || '').toUpperCase() === 'RUNNING')
@ -68,6 +70,7 @@ watch(() => props.history, (history) => {
initialized = true initialized = true
}, { immediate: true, deep: true }) }, { immediate: true, deep: true })
// 后端状态枚举统一映射为 Element Plus tag 的文案和类型。
const statusMeta = (status) => { const statusMeta = (status) => {
const value = String(status || '').toUpperCase() const value = String(status || '').toUpperCase()
if (value === 'SUCCESS') return { label: '执行完成', type: 'success' } if (value === 'SUCCESS') return { label: '执行完成', type: 'success' }
@ -77,6 +80,7 @@ const statusMeta = (status) => {
return { label: '执行中', type: 'warning' } return { label: '执行中', type: 'warning' }
} }
// 循环轮次的分析结论独立于执行状态,主要服务音视频/图片分析类节点的通过性展示。
const analysisMeta = (status) => { const analysisMeta = (status) => {
if (status === 'PASSED') return { label: '分析通过', type: 'success' } if (status === 'PASSED') return { label: '分析通过', type: 'success' }
if (status === 'FAILED') return { label: '分析未通过', type: 'danger' } if (status === 'FAILED') return { label: '分析未通过', type: 'danger' }
@ -84,6 +88,7 @@ const analysisMeta = (status) => {
return null return null
} }
// 汇总所有轮次的分析状态,用于顶部快速提示是否存在未通过的循环结果。
const analysisSummary = computed(() => { const analysisSummary = computed(() => {
const statuses = props.history.map(item => item.analysisStatus).filter(Boolean) const statuses = props.history.map(item => item.analysisStatus).filter(Boolean)
if (!statuses.length) return null if (!statuses.length) return null
@ -92,6 +97,7 @@ const analysisSummary = computed(() => {
return { label: '全部通过', type: 'success' } return { label: '全部通过', type: 'success' }
}) })
// 后端以毫秒返回耗时,短耗时保留 ms,较长耗时转秒,避免标题区域过长。
const formatDuration = (value) => { const formatDuration = (value) => {
const duration = Number(value) const duration = Number(value)
if (!Number.isFinite(duration) || duration < 0) return '-' if (!Number.isFinite(duration) || duration < 0) return '-'
@ -99,6 +105,7 @@ const formatDuration = (value) => {
return `${(duration / 1000).toFixed(2)} s` return `${(duration / 1000).toFixed(2)} s`
} }
// JSON Viewer 只在确实有内容时展示,避免空对象/空数组占据节点详情空间。
const hasValue = (value) => { const hasValue = (value) => {
if (value === null || value === undefined || value === '') return false if (value === null || value === undefined || value === '') return false
if (Array.isArray(value)) return value.length > 0 if (Array.isArray(value)) return value.length > 0

View File

@ -51,6 +51,7 @@ import { CircleCloseFilled, Loading, SuccessFilled, VideoPause, View } from "@el
const props = defineProps({ const props = defineProps({
state: { type: String, default: "NORMAL" }, state: { type: String, default: "NORMAL" },
runtimes: { type: Number, default: 0 }, runtimes: { type: Number, default: 0 },
// 循环和子流程的运行结果更宽,允许调用方覆盖弹窗尺寸和样式类。
popoverWidth: { type: Number, default: 480 }, popoverWidth: { type: Number, default: 480 },
popperClass: { type: String, default: "flow-result-popover" }, popperClass: { type: String, default: "flow-result-popover" },
}); });

View File

@ -91,6 +91,7 @@ const isEditing = ref(false);
const localName = ref(""); const localName = ref("");
const nameInputRef = ref(); const nameInputRef = ref();
const isSelectArea = computed(() => props.nodeType === "selectArea"); const isSelectArea = computed(() => props.nodeType === "selectArea");
// 这些动作只是流程结构或上下文占位,不能脱离完整流程单独执行。
const flowContextActions = new Set([ const flowContextActions = new Set([
"NONE", "NONE",
"START", "START",

View File

@ -81,6 +81,7 @@ const currentComponent = computed(() => {
return null return null
}) })
// 只有子流程参数面板需要当前流程 ID,用于后端过滤不可引用的发布版本。
const componentExtraProps = computed(() => ( const componentExtraProps = computed(() => (
props.data?.type === 'subFlow' ? { flowId: props.flowId } : {} props.data?.type === 'subFlow' ? { flowId: props.flowId } : {}
)) ))

View File

@ -68,6 +68,8 @@ const executions = computed(() => Array.isArray(props.data.executions) ? props.d
const expandedExecutions = ref([]) const expandedExecutions = ref([])
let initialized = false let initialized = false
// 子流程可能被轮询持续刷新,默认展开只做一次,避免用户查看历史分组时被新数据打断。
// 若有正在运行的分组优先展开它,否则展开最新一次执行分组。
watch(executions, (groups) => { watch(executions, (groups) => {
if (initialized || !groups.length) return if (initialized || !groups.length) return
const active = groups.find((group) => String(group.status || '').toUpperCase() === 'RUNNING') const active = groups.find((group) => String(group.status || '').toUpperCase() === 'RUNNING')
@ -75,6 +77,7 @@ watch(executions, (groups) => {
initialized = true initialized = true
}, { immediate: true }) }, { immediate: true })
// 后端状态枚举统一映射为 Element Plus tag 的文案和类型。
const statusMeta = (status) => { const statusMeta = (status) => {
const value = String(status || '').toUpperCase() const value = String(status || '').toUpperCase()
if (value === 'SUCCESS') return { label: '执行完成', type: 'success' } if (value === 'SUCCESS') return { label: '执行完成', type: 'success' }
@ -84,6 +87,7 @@ const statusMeta = (status) => {
return { label: '执行中', type: 'warning' } return { label: '执行中', type: 'warning' }
} }
// 子流程整体的分析结论由内部判定型节点汇总得出,和执行成功/失败状态分开展示。
const analysisMeta = computed(() => { const analysisMeta = computed(() => {
if (props.data.analysisStatus === 'PASSED') return { label: '分析通过', type: 'success' } if (props.data.analysisStatus === 'PASSED') return { label: '分析通过', type: 'success' }
if (props.data.analysisStatus === 'FAILED') return { label: '分析未通过', type: 'danger' } if (props.data.analysisStatus === 'FAILED') return { label: '分析未通过', type: 'danger' }
@ -91,6 +95,7 @@ const analysisMeta = computed(() => {
return null return null
}) })
// 后端以毫秒返回耗时,短耗时保留 ms,较长耗时转秒,避免标题区域过长。
const formatDuration = (value) => { const formatDuration = (value) => {
const duration = Number(value) const duration = Number(value)
if (!Number.isFinite(duration) || duration < 0) return '-' if (!Number.isFinite(duration) || duration < 0) return '-'
@ -98,6 +103,7 @@ const formatDuration = (value) => {
return `${(duration / 1000).toFixed(2)} s` return `${(duration / 1000).toFixed(2)} s`
} }
// JSON Viewer 只在确实有内容时展示,避免空对象/空数组占据节点详情空间。
const hasValue = (value) => { const hasValue = (value) => {
if (value === null || value === undefined || value === '') return false if (value === null || value === undefined || value === '') return false
if (Array.isArray(value)) return value.length > 0 if (Array.isArray(value)) return value.length > 0
@ -131,6 +137,7 @@ const hasValue = (value) => {
min-width: 0; min-width: 0;
flex: 1; flex: 1;
gap: 8px; gap: 8px;
padding-right: 8px; padding-right: 8px;
} }

View File

@ -167,14 +167,20 @@ const parseObject = (value) => {
} }
} }
// 统一合并新版 paramsOut 和旧版 actualResult/extraInfo,后写入的 extraInfo 拥有最高展示优先级。
const outputOf = (log) => ({ const outputOf = (log) => ({
...parseObject(log?.paramsOut), ...parseObject(log?.paramsOut),
...parseObject(log?.actualResult), ...parseObject(log?.actualResult),
...parseObject(log?.extraInfo), ...parseObject(log?.extraInfo),
}) })
// _executionContext 保存后端补充的运行上下文,例如循环层级、事件类型和任务内出现次数。
const executionContextOf = (log) => outputOf(log)._executionContext || {} const executionContextOf = (log) => outputOf(log)._executionContext || {}
/**
* 从新旧字段中读取循环路径,并兼容后端可能返回的数组、数字或分隔字符串。
* 返回空数组表示该日志不属于循环轮次。
*/
const rawIterationPathOf = (log) => { const rawIterationPathOf = (log) => {
const values = [log?.iterations, executionContextOf(log).iterations, log?.iteration] const values = [log?.iterations, executionContextOf(log).iterations, log?.iteration]
for (const value of values) { for (const value of values) {
@ -188,9 +194,8 @@ const rawIterationPathOf = (log) => {
return [] return []
} }
// Historical NODE_STARTED records were persisted before the loop path was copied // 历史 NODE_STARTED 记录入库时还没有写入循环路径,通过同一节点后续终态事件反推,
// 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.
const inferredIterationPaths = computed(() => { const inferredIterationPaths = computed(() => {
const result = new WeakMap() const result = new WeakMap()
const pendingStarts = new Map() const pendingStarts = new Map()
@ -215,19 +220,21 @@ const inferredIterationPaths = computed(() => {
return result return result
}) })
// 优先使用日志自身的循环路径;没有时再使用历史日志的推断结果。
const iterationPathOf = (log) => { const iterationPathOf = (log) => {
const path = rawIterationPathOf(log) const path = rawIterationPathOf(log)
return path.length ? path : inferredIterationPaths.value.get(log) || [] return path.length ? path : inferredIterationPaths.value.get(log) || []
} }
// 节点标识 + 循环路径 + 任务内出现次数共同决定一条开始/结束事件是否属于同一次执行。
const executionKeyOf = (log) => { const executionKeyOf = (log) => {
const context = executionContextOf(log) const context = executionContextOf(log)
const nodeKey = log?.nodeId || log?.executionStep || log?.nodeName || log?.logContent || '' const nodeKey = log?.nodeId || log?.executionStep || log?.nodeName || log?.logContent || ''
return `${nodeKey}:${iterationPathOf(log).join('.')}:${context.itemOccurrence || ''}` 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 visibleLogs = computed(() => {
const source = Array.isArray(props.list) ? props.list : [] const source = Array.isArray(props.list) ? props.list : []
if (isDebugView.value) return source 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(' - ') || [log?.action || log?.nodeType, log?.status].filter(Boolean).join(' - ')
|| '节点执行记录' || '节点执行记录'
/**
* 提取循环折叠标题上的分析判定。非判定型日志返回 null,避免普通节点影响“分析通过”摘要。
*/
const analysisPassedOf = (log) => { const analysisPassedOf = (log) => {
const output = outputOf(log) const output = outputOf(log)
const analysisType = String(output.analysisType || '').toUpperCase() const analysisType = String(output.analysisType || '').toUpperCase()
@ -273,6 +283,10 @@ const toggleIteration = (key) => {
expandedIterations.value = next expandedIterations.value = next
} }
/**
* 根据同一循环轮次内的日志状态推导折叠标题状态。
* 优先级为失败 > 正在执行 > 已完成,兼容数字状态和事件型上下文。
*/
const statusOfIteration = (logs) => { const statusOfIteration = (logs) => {
const statuses = logs.map(log => String(log?.status ?? log?.logStatus ?? '').toUpperCase()) const statuses = logs.map(log => String(log?.status ?? log?.logStatus ?? '').toUpperCase())
const contexts = logs.map(log => String(executionContextOf(log).eventType || '').toUpperCase()) const contexts = logs.map(log => String(executionContextOf(log).eventType || '').toUpperCase())
@ -298,6 +312,10 @@ const statusOfIteration = (logs) => {
return { label: '执行完成', type: 'success' } return { label: '执行完成', type: 'success' }
} }
/**
* 把扁平日志列表转换成“循环标题 + 可展开子日志”的展示序列。
* 这一步只改变展示结构,不修改 props.list 中的原始日志对象。
*/
const displayLogs = computed(() => { const displayLogs = computed(() => {
const source = visibleLogs.value const source = visibleLogs.value
const groups = new Map() const groups = new Map()
@ -340,6 +358,7 @@ const displayLogs = computed(() => {
return result return result
}) })
// 调试模式展示未被分析面板消费的原始输出,方便排查节点执行结果。
const genericOutputOf = (log) => { const genericOutputOf = (log) => {
if (analysisOf(log)) return '' if (analysisOf(log)) return ''
const output = { ...outputOf(log) } const output = { ...outputOf(log) }
@ -349,6 +368,7 @@ const genericOutputOf = (log) => {
return JSON.stringify(output, null, 2) return JSON.stringify(output, null, 2)
} }
// 用户模式只保留最有业务含义的一行摘要,避免把完整 JSON 暴露给非调试场景。
const businessSummaryOf = (log) => { const businessSummaryOf = (log) => {
if (analysisOf(log) || mediaOf(log).length) return '' if (analysisOf(log) || mediaOf(log).length) return ''
const output = { ...outputOf(log) } const output = { ...outputOf(log) }

View File

@ -124,7 +124,6 @@ import { computed, ref, reactive, watch, nextTick } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue' import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue' import FormItemRecursive from '../FormItemRecursive.vue'
import RobotExecutionTarget from './RobotExecutionTarget.vue' import RobotExecutionTarget from './RobotExecutionTarget.vue'
import { getInput } from '@/utils/flow'
import { useQuote } from './useQuote.js' import { useQuote } from './useQuote.js'
import { getArmStatus, getArmJointState, getAgvStatus, getAgvStations } from '@/api/flow/flow' import { getArmStatus, getArmJointState, getAgvStatus, getAgvStations } from '@/api/flow/flow'
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot' import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'

View File

@ -198,7 +198,6 @@ import { ref, reactive, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue' import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue' import FormItemRecursive from '../FormItemRecursive.vue'
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import { getInput } from '@/utils/flow'
import { useQuote } from './useQuote.js' import { useQuote } from './useQuote.js'
const props = defineProps({ const props = defineProps({

View File

@ -203,7 +203,6 @@ import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue' import FormItemRecursive from '../FormItemRecursive.vue'
import RobotExecutionTarget from './RobotExecutionTarget.vue' import RobotExecutionTarget from './RobotExecutionTarget.vue'
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import { getInput } from '@/utils/flow'
import { useQuote } from './useQuote.js' import { useQuote } from './useQuote.js'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'

View File

@ -48,10 +48,15 @@ const quoteOptions = ref([])
const clone = (value) => JSON.parse(JSON.stringify(value ?? null)) const clone = (value) => JSON.parse(JSON.stringify(value ?? null))
const key = () => `output_${Date.now()}_${Math.random().toString(16).slice(2)}` const key = () => `output_${Date.now()}_${Math.random().toString(16).slice(2)}`
// 结束节点的输出只能来自已经执行过的开始参数或上游节点输出,面板展开时实时刷新引用树。
const refreshQuoteOptions = (visible = true) => { const refreshQuoteOptions = (visible = true) => {
if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || [] if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || []
} }
/**
* 根据级联引用路径找到源字段定义,用于继承类型、子字段和默认说明。
* 路径格式为 [节点ID, input/output, 字段名, 子字段名...]。
*/
const findDefinition = (quote) => { const findDefinition = (quote) => {
if (!Array.isArray(quote) || quote.length < 3) return null if (!Array.isArray(quote) || quote.length < 3) return null
const graph = globalThis.lf.getGraphData() const graph = globalThis.lf.getGraphData()
@ -67,6 +72,10 @@ const findDefinition = (quote) => {
return definition return definition
} }
/**
* 用户切换引用来源后同步输出契约的类型和层级结构。
* 未手动填写名称/说明时,优先继承被引用字段的元信息。
*/
const changeQuote = (item, value) => { const changeQuote = (item, value) => {
item.quoteType = value?.[1] || '' item.quoteType = value?.[1] || ''
const definition = findDefinition(value) const definition = findDefinition(value)
@ -104,6 +113,10 @@ const init = () => {
refreshQuoteOptions() refreshQuoteOptions()
} }
/**
* 校验流程输出契约并写回结束节点。
* nodeParams 保存运行时取值引用,outputParams 保存对外暴露的输出字段说明。
*/
const validateAndSave = () => { const validateAndSave = () => {
const names = outputs.value.map((item) => item.name.trim()) const names = outputs.value.map((item) => item.name.trim())
if (names.some((name) => !name)) { if (names.some((name) => !name)) {

View File

@ -339,7 +339,7 @@ import { ref, reactive, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue' import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue' import FormItemRecursive from '../FormItemRecursive.vue'
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import { getInput, transformHttpNodeData } from '@/utils/flow' import { transformHttpNodeData } from '@/utils/flow'
import { useQuote } from './useQuote.js' import { useQuote } from './useQuote.js'
const props = defineProps({ const props = defineProps({

View File

@ -369,7 +369,7 @@ const normalizeStoredTuning = (value, preset) => {
} }
} }
// getInput 根据当前节点 ID 返回可引用的上游输出,只在级联面板打开时刷新以减少无效计算。 // getUpstreamInput 根据当前节点 ID 返回可引用的上游输出,只在级联面板打开时刷新以减少无效计算。
const loadQuoteOptions = (visible) => { const loadQuoteOptions = (visible) => {
if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || [] if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || []
} }

View File

@ -142,7 +142,7 @@
import { ref, reactive, watch, nextTick } from 'vue' import { ref, reactive, watch, nextTick } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue' import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue' import FormItemRecursive from '../FormItemRecursive.vue'
import { getInput, transformSdAgentNodeData } from '@/utils/flow' import { transformSdAgentNodeData } from '@/utils/flow'
import { useQuote } from './useQuote.js' import { useQuote } from './useQuote.js'
const props = defineProps({ const props = defineProps({

View File

@ -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 selectedOptionAvailable = computed(() => options.value.some((item) => item.versionId === selection.versionId))
const projectLabel = computed(() => selection.projectCode === 'AIMA' ? '爱玛' : '长安') const projectLabel = computed(() => selection.projectCode === 'AIMA' ? '爱玛' : '长安')
// 引用来源随画布连线和节点输出变化而变化,级联面板每次展开时重新读取最新上游字段。
const refreshQuoteOptions = (visible = true) => { const refreshQuoteOptions = (visible = true) => {
if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || [] if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || []
} }
/**
* 从节点已有配置恢复子流程选择和输入/输出契约。
* nodeParams 存储的是输入引用,outputParams 存储的是子流程对外暴露的输出定义。
*/
const restore = () => { const restore = () => {
const stored = props.data.properties?.subFlow || {} const stored = props.data.properties?.subFlow || {}
Object.assign(selection, { Object.assign(selection, {
@ -137,6 +142,10 @@ const restore = () => {
refreshQuoteOptions() refreshQuoteOptions()
} }
/**
* 应用一个已发布版本的契约。
* 同名输入会沿用当前已经配置的引用,避免用户同步版本后重复选择所有上游字段。
*/
const applyOption = (option) => { const applyOption = (option) => {
if (!option) return if (!option) return
const existing = new Map(inputs.value.map((item) => [item.name, item])) const existing = new Map(inputs.value.map((item) => [item.name, item]))
@ -174,6 +183,10 @@ const loadOptions = async () => {
} }
} }
/**
* 把子流程需要的机器人角色和设备用途合并到当前流程开始节点。
* 同 roleKey 的资源取更严格的 required/quantity,设备用途类型冲突时直接阻断保存。
*/
const mergeResourceRoles = () => { const mergeResourceRoles = () => {
const graph = globalThis.lf.getGraphData() const graph = globalThis.lf.getGraphData()
const start = graph.nodes.find((node) => node.type === 'start') const start = graph.nodes.find((node) => node.type === 'start')
@ -211,6 +224,10 @@ const mergeResourceRoles = () => {
return { graph, start, roles } return { graph, start, roles }
} }
/**
* 校验并写回子流程节点配置。
* 保存前先在临时图上执行资源清理,确保切换子流程版本后不残留已不用的自动托管设备用途。
*/
const validateAndSave = () => { const validateAndSave = () => {
if (!selection.versionId) { if (!selection.versionId) {
ElMessage.warning('请选择已发布工作流') ElMessage.warning('请选择已发布工作流')

View File

@ -5,13 +5,17 @@ export function useQuote(nodeId) {
const quoteOptions = ref([]) const quoteOptions = ref([])
const cascaderRefs = ref([]) const cascaderRefs = ref([])
// 引用树必须按当前节点重新计算,避免选择到同级后置节点、下游节点或其他循环体内部字段。
const refreshQuoteOptions = () => { const refreshQuoteOptions = () => {
quoteOptions.value = getUpstreamInput(nodeId) || [] quoteOptions.value = getUpstreamInput(nodeId) || []
} }
// The option tree must exist before Cascader can render a persisted path. // Cascader 渲染已保存路径前需要先存在完整 option 树,否则回显会丢失末级字段。
refreshQuoteOptions() refreshQuoteOptions()
/**
* 参数类型在手输和引用之间切换时,清空另一侧的值,避免保存出互相冲突的 input/quote。
*/
const handleTypeChange = (property) => { const handleTypeChange = (property) => {
if (!property) return if (!property) return
if (property.type === 'input') { if (property.type === 'input') {
@ -22,12 +26,16 @@ export function useQuote(nodeId) {
} }
} }
/**
* 级联选择器只把路径写入表单项,quoteType 由路径第二级 input/output 推导。
*/
const cascaderChange = (value, property) => { const cascaderChange = (value, property) => {
if (!property) return if (!property) return
property.quote = Array.isArray(value) ? [...value] : [] property.quote = Array.isArray(value) ? [...value] : []
property.quoteType = value?.[1] === 'input' ? 'input' : 'output' property.quoteType = value?.[1] === 'input' ? 'input' : 'output'
} }
// 面板打开时重新读取上游节点,承接刚刚新增的连线或输出字段。
const visibleChange = (visible) => { const visibleChange = (visible) => {
if (visible) refreshQuoteOptions() if (visible) refreshQuoteOptions()
} }

View File

@ -182,6 +182,7 @@ export const collapseList = [
action: 'SUB_FLOW', action: 'SUB_FLOW',
nodeType: 'NONE', nodeType: 'NONE',
desc: "执行同一项目中已发布的工作流", desc: "执行同一项目中已发布的工作流",
// 具体输入/输出契约在参数面板选择发布版本后同步,配置清单只提供初始空壳。
subFlow: {}, subFlow: {},
nodeParams: [], nodeParams: [],
outputParams: [], outputParams: [],

View File

@ -836,6 +836,7 @@ const versionLoading = ref(false);
const versionList = ref([]); const versionList = ref([]);
const versionPreviewMode = ref(false); const versionPreviewMode = ref(false);
const previewingVersionNo = ref(null); const previewingVersionNo = ref(null);
// 试运行前需要先自动保存草稿,单独的准备态用于避免重复点击触发多次保存/弹窗。
const trialPreparing = ref(false); const trialPreparing = ref(false);
let draftBeforeVersionPreview = null; let draftBeforeVersionPreview = null;
let oldFlowDataBeforeVersionPreview = null; let oldFlowDataBeforeVersionPreview = null;
@ -890,6 +891,7 @@ const testRun = async () => {
return; return;
} }
// 试运行使用后端持久化的草稿数据,先保存能保证画布上的最新变更参与本次执行。
trialPreparing.value = true; trialPreparing.value = true;
try { try {
const saved = await saveDraft({ successMessage: "已自动保存当前草稿" }); const saved = await saveDraft({ successMessage: "已自动保存当前草稿" });
@ -1077,6 +1079,11 @@ const changeState = (value) => {
loopFlowView(value.instId); loopFlowView(value.instId);
}; };
/**
* 从后端运行日志中的内部节点 ID 反推出所属子流程节点。
* 后端存在新旧两类 ID 协议,这里统一归并到外层子流程节点,便于节点状态面板展示内部执行详情。
* @param {string} runtimeNodeId 运行日志中的节点 ID
*/
const resolveSubFlowParentNodeId = (runtimeNodeId) => { const resolveSubFlowParentNodeId = (runtimeNodeId) => {
const markerIndexes = [runtimeNodeId.indexOf("__sf__"), runtimeNodeId.indexOf("__subflow__")] const markerIndexes = [runtimeNodeId.indexOf("__sf__"), runtimeNodeId.indexOf("__subflow__")]
.filter((index) => index > 0); .filter((index) => index > 0);
@ -1093,6 +1100,10 @@ const resolveSubFlowParentNodeId = (runtimeNodeId) => {
return parentNode?.id || null; return parentNode?.id || null;
}; };
/**
* 运行日志中的入参/出参可能是 JSON 字符串,也可能已经是对象;统一转成对象供结果面板消费。
* @param {*} value 运行日志字段值
*/
const parseRuntimeValue = (value) => { const parseRuntimeValue = (value) => {
if (!value) return {}; if (!value) return {};
if (typeof value === "object") return value; if (typeof value === "object") return value;
@ -1104,6 +1115,10 @@ const parseRuntimeValue = (value) => {
} }
}; };
/**
* 兼容数组、单值和分隔字符串三种循环层级格式,返回可用于分组展示的循环路径。
* @param {*} item 运行日志条目
*/
const runtimeIterationPath = (item) => { const runtimeIterationPath = (item) => {
const value = item?.iterations ?? item?.iteration; const value = item?.iterations ?? item?.iteration;
if (Array.isArray(value)) return value.filter(part => part !== null && part !== ""); 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); return String(value).split(/[-.,/]/).map(part => part.trim()).filter(Boolean);
}; };
/**
* 把同一节点在不同循环次数里的日志汇总成 iterationHistory,写回对应节点状态弹窗。
* 这样单个子节点既保留最新状态,也能展开查看每次循环的输入、输出和错误。
* @param {Array} logs 本次轮询返回的运行日志
*/
const emitLoopIterationOutputs = (logs) => { const emitLoopIterationOutputs = (logs) => {
const grouped = new Map(); const grouped = new Map();
logs.forEach((log) => { logs.forEach((log) => {
@ -1150,6 +1170,10 @@ const emitLoopIterationOutputs = (logs) => {
}); });
}; };
/**
* 将后端可能返回的布尔字符串转为三态结果;null 表示无法判断,避免误报“未通过”。
* @param {*} value 待转换值
*/
const runtimeBoolean = (value) => { const runtimeBoolean = (value) => {
if (value === true || value === false) return value; if (value === true || value === false) return value;
if (String(value).toLowerCase() === "true") return true; if (String(value).toLowerCase() === "true") return true;
@ -1157,6 +1181,11 @@ const runtimeBoolean = (value) => {
return null; return null;
}; };
/**
* 识别需要参与通过/未通过汇总的音视频分析节点,并提取业务判定结果。
* @param {*} log 运行日志条目
* @param {*} output 已解析的节点输出
*/
const decisionAnalysisOf = (log, output) => { const decisionAnalysisOf = (log, output) => {
const action = String(log?.action || "").toUpperCase(); const action = String(log?.action || "").toUpperCase();
const analysisType = String(output?.analysisType || "").toUpperCase(); const analysisType = String(output?.analysisType || "").toUpperCase();
@ -1169,6 +1198,7 @@ const decisionAnalysisOf = (log, output) => {
}; };
}; };
// 子流程内部日志只携带动作枚举时,使用这张表转成更适合弹窗展示的中文名称。
const SUB_FLOW_ACTION_LABELS = Object.freeze({ const SUB_FLOW_ACTION_LABELS = Object.freeze({
START: "开始", START: "开始",
END: "结束", END: "结束",
@ -1193,6 +1223,10 @@ const SUB_FLOW_ACTION_LABELS = Object.freeze({
const subFlowActionLabel = (log) => SUB_FLOW_ACTION_LABELS[String(log?.action || "").toUpperCase()] const subFlowActionLabel = (log) => SUB_FLOW_ACTION_LABELS[String(log?.action || "").toUpperCase()]
|| String(log?.action || log?.nodeType || "内部节点"); || String(log?.action || log?.nodeType || "内部节点");
/**
* 优先从日志消息中提取用户配置的节点名;取不到时再退回到动作枚举文案。
* @param {*} log 子流程内部运行日志
*/
const subFlowNodeName = (log) => { const subFlowNodeName = (log) => {
const action = String(log?.action || ""); const action = String(log?.action || "");
const match = String(log?.message || "").match(/^\[([^\]]+)]\s*执行/); const match = String(log?.message || "").match(/^\[([^\]]+)]\s*执行/);
@ -1201,6 +1235,11 @@ const subFlowNodeName = (log) => {
return subFlowActionLabel(log); return subFlowActionLabel(log);
}; };
/**
* 将子流程内部节点日志聚合到外层子流程节点,生成分组后的执行明细和整体分析结论。
* 子流程可能包含循环,因此内部日志按循环路径拆分;普通步骤统一放到 root 分组。
* @param {Array} logs 本次轮询返回的运行日志
*/
const emitSubFlowContainerOutputs = (logs) => { const emitSubFlowContainerOutputs = (logs) => {
const subFlowNodes = (lf.getGraphData().nodes || []).filter(node => node.type === "subFlow"); const subFlowNodes = (lf.getGraphData().nodes || []).filter(node => node.type === "subFlow");
subFlowNodes.forEach((subFlowNode) => { subFlowNodes.forEach((subFlowNode) => {
@ -1212,6 +1251,7 @@ const emitSubFlowContainerOutputs = (logs) => {
const grouped = new Map(); const grouped = new Map();
internalLogs.forEach((log) => { internalLogs.forEach((log) => {
const nodeType = String(log?.nodeType || "").toUpperCase(); const nodeType = String(log?.nodeType || "").toUpperCase();
// 开始/结束这类结构节点没有业务输出,成功时隐藏;失败时保留,避免吞掉异常信息。
if (["START", "END", "SUB_START", "SUB_END"].includes(nodeType) if (["START", "END", "SUB_START", "SUB_END"].includes(nodeType)
&& String(log?.status || "").toUpperCase() !== "FAILED") return; && String(log?.status || "").toUpperCase() !== "FAILED") return;
const iteration = runtimeIterationPath(log); const iteration = runtimeIterationPath(log);
@ -1252,6 +1292,7 @@ const emitSubFlowContainerOutputs = (logs) => {
const allNodes = executions.flatMap(execution => execution.nodes); const allNodes = executions.flatMap(execution => execution.nodes);
const analyses = allNodes.filter(node => node.isAnalysis); const analyses = allNodes.filter(node => node.isAnalysis);
// 有任一分析节点失败即认为子流程分析未通过;仍在执行或未返回判定时保持 RUNNING。
const analysisStatus = analyses.some(node => node.passed === false || String(node.status || "").toUpperCase() === "FAILED") const analysisStatus = analyses.some(node => node.passed === false || String(node.status || "").toUpperCase() === "FAILED")
? "FAILED" ? "FAILED"
: analyses.some(node => node.passed === null || String(node.status || "").toUpperCase() === "RUNNING") : 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 emitLoopContainerOutputs = (logs) => {
const graph = lf.getGraphData(); const graph = lf.getGraphData();
const graphNodes = graph.nodes || []; const graphNodes = graph.nodes || [];
@ -1308,6 +1354,7 @@ const emitLoopContainerOutputs = (logs) => {
.filter(node => String(node.properties?.parentId || "") === String(loopNode.id)) .filter(node => String(node.properties?.parentId || "") === String(loopNode.id))
.map(node => node.id), .map(node => node.id),
].map(String)); ].map(String));
// 如果循环体包含判定型分析节点,即使当前轮次尚未返回 passed,也需要显示“分析中”。
const loopHasDecisionAnalysis = [...childIds].some((childId) => { const loopHasDecisionAnalysis = [...childIds].some((childId) => {
const action = String(nodeById.get(childId)?.properties?.action || "").toUpperCase(); const action = String(nodeById.get(childId)?.properties?.action || "").toUpperCase();
return ["AUDIO_EVENT_CLASSIFY", "VIDEO_ANALYZE"].includes(action); return ["AUDIO_EVENT_CLASSIFY", "VIDEO_ANALYZE"].includes(action);

View File

@ -43,6 +43,7 @@ const runtimes = ref(0)
const inputJsonData = ref({}) const inputJsonData = ref({})
const outputJsonData = ref({}) const outputJsonData = ref({})
const errorInfoData = ref('') const errorInfoData = ref('')
// 优先使用主页面轮询聚合出的 subFlowExecution;没有内部日志时退回到普通输出,保证旧数据仍可展示。
const subFlowExecution = computed(() => outputJsonData.value?.subFlowExecution || { const subFlowExecution = computed(() => outputJsonData.value?.subFlowExecution || {
name: props.properties?.subFlow?.detectName || props.properties?.name, name: props.properties?.subFlow?.detectName || props.properties?.name,
versionNo: props.properties?.subFlow?.versionNo, versionNo: props.properties?.subFlow?.versionNo,
@ -50,6 +51,7 @@ const subFlowExecution = computed(() => outputJsonData.value?.subFlowExecution |
executions: [], executions: [],
}) })
// 子流程输出来自后端运行日志,兼容 JSON 字符串和对象;解析失败时保留原值用于排查。
const parseValue = (value) => { const parseValue = (value) => {
if (!value) return {} if (!value) return {}
if (typeof value === 'object') return value if (typeof value === 'object') return value
@ -65,6 +67,7 @@ const setNodeName = (name) => {
onMounted(() => { onMounted(() => {
onFlowEvent('changeNodeState', (data) => { onFlowEvent('changeNodeState', (data) => {
if (data.nodeId !== props.model.id) return if (data.nodeId !== props.model.id) return
// 外层子流程节点状态由主页面统一聚合内部日志后派发,这里只负责同步到节点 UI。
nodeOperatingStatus.value = data.status nodeOperatingStatus.value = data.status
runtimes.value = data.endTime && data.startTime ? data.endTime - data.startTime : 0 runtimes.value = data.endTime && data.startTime ? data.endTime - data.startTime : 0
inputJsonData.value = parseValue(data.paramsIn) inputJsonData.value = parseValue(data.paramsIn)

View File

@ -94,6 +94,7 @@ const props = defineProps({
connectable: [Boolean, Number, String, Function], connectable: [Boolean, Number, String, Function],
}); });
// 画布节点类型到 Vue 组件的唯一映射表;新增节点类型时需要在这里接入渲染组件。
const componentMap = { const componentMap = {
start: StartNode, start: StartNode,
end: EndNode, end: EndNode,