From 16481996b90f94dde823ac606ea23424711bb238 Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Wed, 12 Aug 2026 17:57:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0=E5=8A=A8?= =?UTF-8?q?=E4=BD=9C=E9=98=9F=E5=88=97=E8=8A=82=E7=82=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 actionQueue 节点类型,支持在边缘端按顺序连续执行多个动作 - 实现 ActionQueueParams 组件,提供执行设置和动作步骤配置界面 - 添加 ActionQueueSingleRun 组件,支持动作队列的单节点执行 - 更新 flowResources 工具函数,完善设备绑定和资源管理逻辑 - 集成动作队列到流程编辑器,支持参数配置和设备分配 - 优化节点渲染显示,支持多设备用途的角色展示 --- src/utils/flowResources.js | 59 +++- .../flow/components/ActionQueueSingleRun.vue | 111 +++++++ src/views/flow/components/ParamsDrawer.vue | 2 + .../components/params/ActionQueueParams.vue | 293 ++++++++++++++++++ src/views/flow/config.js | 19 ++ src/views/flow/index.vue | 44 ++- src/views/flow/nodes/function/actionQueue.vue | 25 ++ src/views/flow/vue-flow/FlowNode.vue | 4 + 8 files changed, 537 insertions(+), 20 deletions(-) create mode 100644 src/views/flow/components/ActionQueueSingleRun.vue create mode 100644 src/views/flow/components/params/ActionQueueParams.vue create mode 100644 src/views/flow/nodes/function/actionQueue.vue diff --git a/src/utils/flowResources.js b/src/utils/flowResources.js index 9b0cb4e..a761b30 100644 --- a/src/utils/flowResources.js +++ b/src/utils/flowResources.js @@ -62,6 +62,7 @@ export const getResourceRoles = (graph = globalThis.lf?.getGraphData?.()) => ( ) const isRobotNode = (node) => node?.properties?.nodeType === 'EDGE' +export const ACTION_QUEUE_ACTION = 'SYSTEM_EXECUTE_ACTION_QUEUE' export const actionRequiresDevice = (action) => ( getDeviceKindForAction(action) != null || action === 'DEVICE_EXECUTE_JSON_COMMAND' ) @@ -139,7 +140,9 @@ export const normalizeFlowResourceSchema = (sourceGraph) => { } target.roleKey ||= defaultRole.roleKey target.dispatchPolicy ||= 'ONE' - target.deviceSlotKey ||= target.deviceBindings?.[0]?.slotKey || '' + const isActionQueue = properties.action === ACTION_QUEUE_ACTION + if (!isActionQueue) target.deviceSlotKey ||= target.deviceBindings?.[0]?.slotKey || '' + target.deviceBindings = Array.isArray(target.deviceBindings) ? target.deviceBindings : [] const targetRole = roles.find((role) => role.roleKey === target.roleKey) || defaultRole const deviceParam = properties.nodeParams?.find((param) => param.name === 'deviceId') @@ -147,11 +150,15 @@ export const normalizeFlowResourceSchema = (sourceGraph) => { const slot = findOrCreateLegacySlot(targetRole, node, legacySlotByIdentity, deviceParam?.input) if (slot) target.deviceSlotKey = slot.slotKey } - const referencedSlot = targetRole.deviceSlots.find((slot) => slot.slotKey === target.deviceSlotKey) - if (referencedSlot && !referencedSlot.managedBy) { - referencedSlot.managedBy = FLOW_NODE_RESOURCE_OWNER - } - delete target.deviceBindings + const referencedSlotKeys = new Set([ + target.deviceSlotKey, + ...target.deviceBindings.map((binding) => binding?.slotKey), + ].filter(Boolean)) + targetRole.deviceSlots.forEach((slot) => { + if (referencedSlotKeys.has(slot.slotKey) && !slot.managedBy) slot.managedBy = FLOW_NODE_RESOURCE_OWNER + }) + if (!isActionQueue) delete target.deviceBindings + if (isActionQueue) delete target.deviceSlotKey properties.nodeParams = (properties.nodeParams || []).filter((param) => param.name !== 'deviceId') properties.executionTarget = target }) @@ -182,9 +189,11 @@ export const pruneUnusedNodeResources = (sourceGraph) => { const target = node.properties?.executionTarget if (!target?.roleKey) return usedRoleKeys.add(target.roleKey) - if (!target.deviceSlotKey) return if (!usedSlotsByRole.has(target.roleKey)) usedSlotsByRole.set(target.roleKey, new Set()) - usedSlotsByRole.get(target.roleKey).add(target.deviceSlotKey) + if (target.deviceSlotKey) usedSlotsByRole.get(target.roleKey).add(target.deviceSlotKey) + ;(target.deviceBindings || []).forEach((binding) => { + if (binding?.slotKey) usedSlotsByRole.get(target.roleKey).add(binding.slotKey) + }) }) let removedSlots = 0 @@ -240,6 +249,13 @@ export const validateFlowResources = (sourceGraph) => { if (target.deviceSlotKey && !(role.deviceSlots || []).some((slot) => slot.slotKey === target.deviceSlotKey)) { errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的设备用途不存在` }) } + ;(target.deviceBindings || []).forEach((binding) => { + if (!binding?.parameterName || !binding?.slotKey) { + errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}存在未配置的队列设备` }) + } else if (!(role.deviceSlots || []).some((slot) => slot.slotKey === binding.slotKey)) { + errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的队列设备用途不存在` }) + } + }) if (nodeRequiresDevice(node) && !target.deviceSlotKey) { errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}未选择设备用途` }) @@ -290,21 +306,34 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo targetRole ||= targetRoles[0] target.roleKey = targetRole.roleKey - if (target.deviceSlotKey && !targetRole.deviceSlots?.some((slot) => slot.slotKey === target.deviceSlotKey)) { - const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === target.deviceSlotKey) + const remapSlot = (slotKey) => { + if (!slotKey || targetRole.deviceSlots?.some((slot) => slot.slotKey === slotKey)) return slotKey + const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === slotKey) const sameSlot = targetRole.deviceSlots?.find((slot) => ( sourceSlot && slot.displayName === sourceSlot.displayName && Number(slot.deviceKind) === Number(sourceSlot.deviceKind) )) - if (sameSlot) { - target.deviceSlotKey = sameSlot.slotKey - return - } + if (sameSlot) return sameSlot.slotKey const newSlot = sourceSlot ? clone(sourceSlot) : createDeviceSlot(getDeviceKindForAction(node.properties?.action), targetRole.deviceSlots) newSlot.managedBy = FLOW_NODE_RESOURCE_OWNER delete newSlot.legacyDeviceId targetRole.deviceSlots.push(newSlot) - target.deviceSlotKey = newSlot.slotKey addedSlots += 1 + return newSlot.slotKey + } + target.deviceSlotKey = remapSlot(target.deviceSlotKey) + ;(target.deviceBindings || []).forEach((binding) => { + binding.slotKey = remapSlot(binding.slotKey) + }) + const bindingSlotByName = new Map((target.deviceBindings || []).map((binding) => ( + [binding.parameterName, binding.slotKey] + ))) + const queueStepsParam = node.properties?.nodeParams?.find((param) => param.name === 'steps') + if (Array.isArray(queueStepsParam?.input)) { + queueStepsParam.input.forEach((step) => { + if (bindingSlotByName.has(step?.deviceBindingKey)) { + step.deviceSlotKey = bindingSlotByName.get(step.deviceBindingKey) + } + }) } }) diff --git a/src/views/flow/components/ActionQueueSingleRun.vue b/src/views/flow/components/ActionQueueSingleRun.vue new file mode 100644 index 0000000..cc23bf8 --- /dev/null +++ b/src/views/flow/components/ActionQueueSingleRun.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index ace2c79..5c76987 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -37,6 +37,7 @@ import SdAgentNodeParams from './params/SdAgentNodeParams.vue' import RecognizeNodeParams from './params/RecognizeNodeParams.vue' import DeviceUniversal from './params/DeviceUniversal.vue' import SwitchNodeParams from './params/SwitchNodeParams.vue' +import ActionQueueParams from './params/ActionQueueParams.vue' const props = defineProps({ drawer: Boolean, @@ -63,6 +64,7 @@ const currentComponent = computed(() => { if (type === 'sdAgent') return SdAgentNodeParams if (type === 'recognize') return RecognizeNodeParams if (['deviceUniversal', 'device_universal'].includes(type)) return DeviceUniversal + if (type === 'actionQueue') return ActionQueueParams if (type === 'branch') return SwitchNodeParams return null }) diff --git a/src/views/flow/components/params/ActionQueueParams.vue b/src/views/flow/components/params/ActionQueueParams.vue new file mode 100644 index 0000000..2a48ce9 --- /dev/null +++ b/src/views/flow/components/params/ActionQueueParams.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/src/views/flow/config.js b/src/views/flow/config.js index a75eb3d..9f47e60 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -337,6 +337,25 @@ function handler(params) { outputParams: [], outputType: 'json' }, + { + icon: planSvg, + name: "动作队列", + type: "actionQueue", + desc: "在边缘端按顺序连续执行多个动作", + action: 'SYSTEM_EXECUTE_ACTION_QUEUE', + nodeType: 'EDGE', + nodeParams: [ + { name: "actionId", type: "input", input: "", required: false }, + { name: "totalTimeoutMs", type: "input", componentType: 'number', input: 60000, required: true }, + { name: "steps", type: "input", input: [], required: true }, + ], + outputParams: [ + { name: 'actionId', type: 'string', desc: '动作队列标识' }, + { name: 'completedSteps', type: 'number', desc: '已完成步骤数' }, + { name: 'result', type: 'string', desc: '执行结果' }, + ], + outputType: 'json' + }, { icon: videoSvg, name: "机械臂", diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue index 4753b93..7d45c71 100644 --- a/src/views/flow/index.vue +++ b/src/views/flow/index.vue @@ -159,7 +159,7 @@ /> - +