From e3a2761683a4d8cc9f3e31b342ceca48886fc663 Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Tue, 11 Aug 2026 17:16:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(flow):=20=E8=A7=A3=E5=86=B3=E9=9D=9E?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=E8=8A=82=E7=82=B9=E6=90=BA=E5=B8=A6?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=E6=89=A7=E8=A1=8C=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E5=90=8E=E7=AB=AF=E8=AF=AF=E5=88=A4?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 非机器人节点不再携带 executionTarget 属性,避免后端将空对象误判为无效角色引用 - 添加 FLOW_NODE_RESOURCE_OWNER 和 USER_RESOURCE_OWNER 常量用于资源归属管理 - 为流程节点创建的设备插槽添加 managedBy 字段标识资源归属 - 移除未被任何节点引用的自动注册角色定义和设备使用 - 为用户手动创建的角色和设备插槽标记为 USER_RESOURCE_OWNER 归属 - 在节点更新时自动清理未使用的节点资源,优化资源管理 --- src/utils/flowResources.js | 66 ++++++++++++++++++- .../components/params/BasicNodeParams.vue | 8 ++- .../flow/components/params/StartParams.vue | 7 +- src/views/flow/index.vue | 5 ++ 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/utils/flowResources.js b/src/utils/flowResources.js index 76f3b00..dedd8c3 100644 --- a/src/utils/flowResources.js +++ b/src/utils/flowResources.js @@ -2,6 +2,8 @@ import { getDeviceKindForAction } from '@/utils/robotDevice' export const FLOW_SCHEMA_VERSION = 2 export const DEFAULT_ROLE_KEY = 'default_executor' +export const FLOW_NODE_RESOURCE_OWNER = 'FLOW_NODE' +export const USER_RESOURCE_OWNER = 'USER' const DEVICE_KIND_META = Object.freeze({ 1: { key: 'agv', name: '移动底盘' }, @@ -75,6 +77,7 @@ const findOrCreateLegacySlot = (role, node, legacySlotByIdentity, legacyDeviceId if (legacySlotByIdentity.has(identity)) return legacySlotByIdentity.get(identity) const slot = createDeviceSlot(kind, role.deviceSlots) + slot.managedBy = FLOW_NODE_RESOURCE_OWNER if (normalizedDeviceId) slot.legacyDeviceId = normalizedDeviceId role.deviceSlots.push(slot) legacySlotByIdentity.set(identity, slot) @@ -117,8 +120,11 @@ export const normalizeFlowResourceSchema = (sourceGraph) => { })) graph.nodes.forEach((node) => { - if (!isRobotNode(node)) return const properties = node.properties || (node.properties = {}) + if (!isRobotNode(node)) { + delete properties.executionTarget + return + } const target = properties.executionTarget || { roleKey: defaultRole.roleKey, dispatchPolicy: 'ONE', @@ -133,6 +139,10 @@ 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 properties.nodeParams = (properties.nodeParams || []).filter((param) => param.name !== 'deviceId') properties.executionTarget = target @@ -147,6 +157,53 @@ export const ensureNodeExecutionResources = (sourceGraph, nodeId) => { return { graph, node, start: getStartNode(graph) } } +/** + * Removes role definitions and device usages that were registered automatically + * for flow nodes and are no longer referenced by any remaining node. + */ +export const pruneUnusedNodeResources = (sourceGraph) => { + const graph = clone(sourceGraph || { nodes: [], edges: [] }) + const start = getStartNode(graph) + if (!start?.properties?.resourceRoles) { + return { graph, start, removedRoles: 0, removedSlots: 0 } + } + + const usedRoleKeys = new Set() + const usedSlotsByRole = new Map() + graph.nodes.filter(isRobotNode).forEach((node) => { + 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) + }) + + let removedSlots = 0 + start.properties.resourceRoles.forEach((role) => { + const usedSlotKeys = usedSlotsByRole.get(role.roleKey) || new Set() + role.deviceSlots = (role.deviceSlots || []).filter((slot) => { + const shouldRemove = slot.managedBy === FLOW_NODE_RESOURCE_OWNER && !usedSlotKeys.has(slot.slotKey) + if (shouldRemove) removedSlots += 1 + return !shouldRemove + }) + }) + + const beforeRoleCount = start.properties.resourceRoles.length + start.properties.resourceRoles = start.properties.resourceRoles.filter((role, index) => ( + index === 0 + || role.managedBy !== FLOW_NODE_RESOURCE_OWNER + || usedRoleKeys.has(role.roleKey) + )) + + return { + graph, + start, + removedRoles: beforeRoleCount - start.properties.resourceRoles.length, + removedSlots, + } +} + export const validateFlowResources = (sourceGraph) => { const graph = normalizeFlowResourceSchema(sourceGraph) const roles = getResourceRoles(graph) @@ -212,8 +269,12 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo if (!targetRole && sourceRole) { targetRole = clone(sourceRole) + targetRole.managedBy = FLOW_NODE_RESOURCE_OWNER delete targetRole.legacyRobotId - ;(targetRole.deviceSlots || []).forEach((slot) => delete slot.legacyDeviceId) + ;(targetRole.deviceSlots || []).forEach((slot) => { + slot.managedBy = FLOW_NODE_RESOURCE_OWNER + delete slot.legacyDeviceId + }) targetRoles.push(targetRole) addedRoles += 1 addedSlots += targetRole.deviceSlots?.length || 0 @@ -231,6 +292,7 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo return } 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 diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue index ce031bb..bda2f92 100644 --- a/src/views/flow/components/params/BasicNodeParams.vue +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -237,11 +237,13 @@ const validateAndSave = async () => { ElMessage.warning('请选择该节点使用的设备用途') } - // 保存到 lf - lf.setProperties(props.data.id, { + // 非机器人节点不应携带机器人执行目标,避免后端将空对象误判为无效角色引用。 + const properties = { ...props.data.properties, ...formData - }) + } + if (!isRobotNode.value) delete properties.executionTarget + lf.setProperties(props.data.id, properties) if (inputValid && outputValid) { emit('save-success') diff --git a/src/views/flow/components/params/StartParams.vue b/src/views/flow/components/params/StartParams.vue index 2cfedf1..b6c57c6 100644 --- a/src/views/flow/components/params/StartParams.vue +++ b/src/views/flow/components/params/StartParams.vue @@ -69,6 +69,7 @@ import { createDeviceSlot, createResourceKey, normalizeFlowResourceSchema, + USER_RESOURCE_OWNER, } from '@/utils/flowResources' const props = defineProps({ data: Object }) @@ -93,11 +94,15 @@ const addRole = () => { ...createDefaultRobotRole(), roleKey: createResourceKey('robot_role'), displayName: `机器人角色${formData.resourceRoles.length + 1}`, + managedBy: USER_RESOURCE_OWNER, }) } const addDeviceSlot = (role) => { - role.deviceSlots.push(createDeviceSlot(null, role.deviceSlots)) + role.deviceSlots.push({ + ...createDeviceSlot(null, role.deviceSlots), + managedBy: USER_RESOURCE_OWNER, + }) } const referencedNodes = (predicate) => globalThis.lf.getGraphData().nodes.filter((node) => predicate(node.properties?.executionTarget)) diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue index c67cdf7..97d7f9c 100644 --- a/src/views/flow/index.vue +++ b/src/views/flow/index.vue @@ -275,6 +275,7 @@ import { getResourceRoles, mergeCopiedResourceRoles, normalizeFlowResourceSchema, + pruneUnusedNodeResources, validateFlowResources, } from "@/utils/flowResources"; import { @@ -1702,6 +1703,10 @@ onMounted(() => { if (index !== -1) { nodeErrorList.value.splice(index, 1); } + const cleanup = pruneUnusedNodeResources(lf.getGraphData()); + if (cleanup.start && (cleanup.removedRoles || cleanup.removedSlots)) { + lf.setProperties(cleanup.start.id, cleanup.start.properties); + } }) });