lxl-dev #3
@ -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',
|
||||
|
||||
@ -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<string, string>} 子节点到父节点的映射。
|
||||
*/
|
||||
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<string>} 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<string>} 目标节点可引用的上游节点 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 = {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" },
|
||||
});
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -81,6 +81,7 @@ const currentComponent = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
// 只有子流程参数面板需要当前流程 ID,用于后端过滤不可引用的发布版本。
|
||||
const componentExtraProps = computed(() => (
|
||||
props.data?.type === 'subFlow' ? { flowId: props.flowId } : {}
|
||||
))
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) }
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -369,7 +369,7 @@ const normalizeStoredTuning = (value, preset) => {
|
||||
}
|
||||
}
|
||||
|
||||
// getInput 根据当前节点 ID 返回可引用的上游输出,只在级联面板打开时刷新以减少无效计算。
|
||||
// getUpstreamInput 根据当前节点 ID 返回可引用的上游输出,只在级联面板打开时刷新以减少无效计算。
|
||||
const loadQuoteOptions = (visible) => {
|
||||
if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || []
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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('请选择已发布工作流')
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -182,6 +182,7 @@ export const collapseList = [
|
||||
action: 'SUB_FLOW',
|
||||
nodeType: 'NONE',
|
||||
desc: "执行同一项目中已发布的工作流",
|
||||
// 具体输入/输出契约在参数面板选择发布版本后同步,配置清单只提供初始空壳。
|
||||
subFlow: {},
|
||||
nodeParams: [],
|
||||
outputParams: [],
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -94,6 +94,7 @@ const props = defineProps({
|
||||
connectable: [Boolean, Number, String, Function],
|
||||
});
|
||||
|
||||
// 画布节点类型到 Vue 组件的唯一映射表;新增节点类型时需要在这里接入渲染组件。
|
||||
const componentMap = {
|
||||
start: StartNode,
|
||||
end: EndNode,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user