import { getDeviceKindForAction } from '@/utils/robotDevice' export const FLOW_SCHEMA_VERSION = 2 export const DEFAULT_ROLE_KEY = 'default_executor' const DEVICE_KIND_META = Object.freeze({ 1: { key: 'agv', name: '移动底盘' }, 2: { key: 'arm', name: '机械臂' }, 4: { key: 'bio_head', name: '仿生头' }, 5: { key: 'camera', name: '摄像头' }, 9: { key: 'microphone', name: '麦克风' }, 12: { key: 'robot', name: '机器人本体' }, 13: { key: 'speaker', name: '扬声器' }, }) const clone = (value) => JSON.parse(JSON.stringify(value ?? null)) const normalizedText = (value) => String(value || '').trim() export const createResourceKey = (prefix) => { const random = globalThis.crypto?.randomUUID?.().replace(/-/g, '').slice(0, 10) || `${Date.now()}${Math.random().toString(16).slice(2, 8)}` return `${prefix}_${random}` } export const createDefaultRobotRole = (legacyRobotId = '') => ({ roleKey: DEFAULT_ROLE_KEY, resourceType: 'ROBOT', displayName: '执行机器人', required: true, quantity: { min: 1, max: 1 }, deviceSlots: [], ...(legacyRobotId ? { legacyRobotId } : {}), }) export const createDeviceSlot = (deviceKind, existingSlots = [], displayName = '') => { const meta = DEVICE_KIND_META[Number(deviceKind)] || { key: 'device', name: '执行设备' } const usedKeys = new Set(existingSlots.map((slot) => slot.slotKey)) let index = 1 let slotKey = meta.key while (usedKeys.has(slotKey)) slotKey = `${meta.key}_${++index}` const sameKindCount = existingSlots.filter((slot) => Number(slot.deviceKind) === Number(deviceKind)).length return { slotKey, displayName: displayName || `${meta.name}${sameKindCount ? sameKindCount + 1 : ''}`, deviceKind: deviceKind == null ? null : Number(deviceKind), required: true, allowReuse: false, } } export const getStartNode = (graph = globalThis.lf?.getGraphData?.()) => ( graph?.nodes?.find((node) => node.type === 'start') ) export const getResourceRoles = (graph = globalThis.lf?.getGraphData?.()) => ( clone(getStartNode(graph)?.properties?.resourceRoles || []) || [] ) const isRobotNode = (node) => node?.properties?.nodeType === 'EDGE' export const actionRequiresDevice = (action) => ( getDeviceKindForAction(action) != null || action === 'DEVICE_EXECUTE_JSON_COMMAND' ) export const nodeRequiresDevice = (node) => actionRequiresDevice(node?.properties?.action) const resolveLegacyRobotId = (startProperties) => ( startProperties?.inputParams?.find((item) => item.name === 'robotId')?.input || startProperties?.robotId || '' ) const findOrCreateLegacySlot = (role, node, legacySlotByIdentity, legacyDeviceId = '') => { const kind = getDeviceKindForAction(node.properties?.action) const normalizedDeviceId = normalizedText(legacyDeviceId) const identity = `${role.roleKey}:${kind ?? 'any'}:${normalizedDeviceId || 'default'}` if (legacySlotByIdentity.has(identity)) return legacySlotByIdentity.get(identity) const slot = createDeviceSlot(kind, role.deviceSlots) if (normalizedDeviceId) slot.legacyDeviceId = normalizedDeviceId role.deviceSlots.push(slot) legacySlotByIdentity.set(identity, slot) return slot } /** * Loads old graphs as Schema V2 without mutating the source object. Physical IDs are * retained only as migration hints and are consumed by the trial-run assignment form. */ export const normalizeFlowResourceSchema = (sourceGraph) => { const graph = clone(sourceGraph || { nodes: [], edges: [] }) const start = getStartNode(graph) if (!start) return graph const startProperties = start.properties || (start.properties = {}) const legacyRobotId = resolveLegacyRobotId(startProperties) const roles = Array.isArray(startProperties.resourceRoles) && startProperties.resourceRoles.length ? startProperties.resourceRoles : [createDefaultRobotRole(legacyRobotId)] roles.forEach((role, index) => { role.roleKey ||= index === 0 ? DEFAULT_ROLE_KEY : createResourceKey('robot_role') role.resourceType = 'ROBOT' role.displayName ||= `机器人角色${index + 1}` role.required = role.required !== false role.quantity ||= { min: 1, max: 1 } role.deviceSlots = Array.isArray(role.deviceSlots) ? role.deviceSlots : [] }) startProperties.schemaVersion = FLOW_SCHEMA_VERSION startProperties.resourceRoles = roles startProperties.inputParams = (startProperties.inputParams || []).filter((item) => item.name !== 'robotId') const defaultRole = roles[0] const legacySlotByIdentity = new Map() roles.forEach((role) => role.deviceSlots.forEach((slot) => { const identity = `${role.roleKey}:${slot.deviceKind ?? 'any'}:${slot.legacyDeviceId || 'default'}` legacySlotByIdentity.set(identity, slot) })) graph.nodes.forEach((node) => { if (!isRobotNode(node)) return const properties = node.properties || (node.properties = {}) const target = properties.executionTarget || { roleKey: defaultRole.roleKey, dispatchPolicy: 'ONE', } target.roleKey ||= defaultRole.roleKey target.dispatchPolicy ||= 'ONE' target.deviceSlotKey ||= target.deviceBindings?.[0]?.slotKey || '' const targetRole = roles.find((role) => role.roleKey === target.roleKey) || defaultRole const deviceParam = properties.nodeParams?.find((param) => param.name === 'deviceId') if (nodeRequiresDevice(node) && !target.deviceSlotKey) { const slot = findOrCreateLegacySlot(targetRole, node, legacySlotByIdentity, deviceParam?.input) if (slot) target.deviceSlotKey = slot.slotKey } delete target.deviceBindings properties.nodeParams = (properties.nodeParams || []).filter((param) => param.name !== 'deviceId') properties.executionTarget = target }) return graph } export const ensureNodeExecutionResources = (sourceGraph, nodeId) => { const graph = normalizeFlowResourceSchema(sourceGraph) const node = graph.nodes.find((item) => item.id === nodeId) return { graph, node, start: getStartNode(graph) } } export const validateFlowResources = (sourceGraph) => { const graph = normalizeFlowResourceSchema(sourceGraph) const roles = getResourceRoles(graph) const errors = [] const roleKeys = new Set() roles.forEach((role) => { if (!normalizedText(role.displayName)) errors.push({ nodeId: getStartNode(graph)?.id, message: '机器人角色名称不能为空' }) if (roleKeys.has(role.roleKey)) errors.push({ nodeId: getStartNode(graph)?.id, message: `机器人角色标识重复:${role.roleKey}` }) roleKeys.add(role.roleKey) const slotKeys = new Set() ;(role.deviceSlots || []).forEach((slot) => { if (!normalizedText(slot.displayName)) errors.push({ nodeId: getStartNode(graph)?.id, message: `${role.displayName}存在未命名的设备用途` }) if (slotKeys.has(slot.slotKey)) errors.push({ nodeId: getStartNode(graph)?.id, message: `${role.displayName}的设备用途标识重复` }) slotKeys.add(slot.slotKey) }) }) graph.nodes.filter(isRobotNode).forEach((node) => { const target = node.properties?.executionTarget const role = roles.find((item) => item.roleKey === target?.roleKey) if (!role) { errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}未选择有效的机器人角色` }) return } if (target.deviceSlotKey && !(role.deviceSlots || []).some((slot) => slot.slotKey === target.deviceSlotKey)) { 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}未选择设备用途` }) } }) return { graph, errors } } export const sanitizeFlowResourceDefinition = (sourceGraph) => { const graph = normalizeFlowResourceSchema(sourceGraph) const start = getStartNode(graph) ;(start?.properties?.resourceRoles || []).forEach((role) => { delete role.legacyRobotId ;(role.deviceSlots || []).forEach((slot) => delete slot.legacyDeviceId) }) graph.nodes.filter(isRobotNode).forEach((node) => { node.properties.nodeParams = (node.properties?.nodeParams || []).filter((param) => param.name !== 'deviceId') }) return graph } export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNodes = []) => { const graph = normalizeFlowResourceSchema(targetGraph) const start = getStartNode(graph) const targetRoles = start.properties.resourceRoles let addedRoles = 0 let addedSlots = 0 copiedNodes.filter(isRobotNode).forEach((node) => { const target = node.properties?.executionTarget if (!target?.roleKey) return const sourceRole = sourceRoles.find((role) => role.roleKey === target.roleKey) let targetRole = targetRoles.find((role) => role.roleKey === target.roleKey) || targetRoles.find((role) => sourceRole && role.displayName === sourceRole.displayName) if (!targetRole && sourceRole) { targetRole = clone(sourceRole) delete targetRole.legacyRobotId ;(targetRole.deviceSlots || []).forEach((slot) => delete slot.legacyDeviceId) targetRoles.push(targetRole) addedRoles += 1 addedSlots += targetRole.deviceSlots?.length || 0 } 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 sameSlot = targetRole.deviceSlots?.find((slot) => ( sourceSlot && slot.displayName === sourceSlot.displayName && Number(slot.deviceKind) === Number(sourceSlot.deviceKind) )) if (sameSlot) { target.deviceSlotKey = sameSlot.slotKey return } const newSlot = sourceSlot ? clone(sourceSlot) : createDeviceSlot(getDeviceKindForAction(node.properties?.action), targetRole.deviceSlots) delete newSlot.legacyDeviceId targetRole.deviceSlots.push(newSlot) target.deviceSlotKey = newSlot.slotKey addedSlots += 1 } }) return { graph, start, addedRoles, addedSlots } } export const getDeviceKindName = (deviceKind) => DEVICE_KIND_META[Number(deviceKind)]?.name || '任意设备' export const mergeResourceRoleDefinitions = (roleGroups = []) => { const merged = [] roleGroups.flat().forEach((sourceRole) => { let role = merged.find((item) => item.roleKey === sourceRole.roleKey) if (!role) { merged.push(clone(sourceRole)) return } ;(sourceRole.deviceSlots || []).forEach((sourceSlot) => { if (!role.deviceSlots.some((slot) => slot.slotKey === sourceSlot.slotKey)) { role.deviceSlots.push(clone(sourceSlot)) } }) }) return merged }