diff --git a/src/api/resource/visionCalibration.js b/src/api/resource/visionCalibration.js new file mode 100644 index 0000000..7e9728b --- /dev/null +++ b/src/api/resource/visionCalibration.js @@ -0,0 +1,55 @@ +import request from '@/utils/request' + +const base = '/vision/calibration' + +export function listCalibrationProfiles(params) { + return request({ url: `${base}/profiles`, method: 'get', params }) +} + +export function getCalibrationProfile(id) { + return request({ url: `${base}/profiles/${id}`, method: 'get' }) +} + +export function updateCalibrationCorrection(id, data) { + return request({ url: `${base}/profiles/${id}/correction`, method: 'put', data }) +} + +export function activateCalibrationProfile(id, forced = false) { + return request({ url: `${base}/profiles/${id}/activate`, method: 'put', data: { forced } }) +} + +export function invalidateCalibrationProfile(id) { + return request({ url: `${base}/profiles/${id}`, method: 'delete' }) +} + +export function startHandEyeCalibration(data) { + return request({ url: `${base}/sessions/hand-eye`, method: 'post', data }) +} + +export function startToolCalibration(data) { + return request({ url: `${base}/sessions/tool`, method: 'post', data }) +} + +export function startCalibrationVerification(id) { + return request({ url: `${base}/profiles/${id}/verify`, method: 'post' }) +} + +export function calibrationHeartbeat(id) { + return request({ url: `${base}/sessions/${id}/heartbeat`, method: 'post' }) +} + +export function getCalibrationSession(id) { + return request({ url: `${base}/sessions/${id}`, method: 'get' }) +} + +export function stopCalibrationSession(id) { + return request({ url: `${base}/sessions/${id}/stop`, method: 'post' }) +} + +export function recordToolPivot(id) { + return request({ url: `${base}/sessions/${id}/tool/pivot`, method: 'post' }) +} + +export function recordToolDirection(id) { + return request({ url: `${base}/sessions/${id}/tool/direction`, method: 'post' }) +} diff --git a/src/utils/flowResources.js b/src/utils/flowResources.js index b583464..ab71a3e 100644 --- a/src/utils/flowResources.js +++ b/src/utils/flowResources.js @@ -77,6 +77,11 @@ export const getResourceRoles = (graph = globalThis.lf?.getGraphData?.()) => ( const isRobotNode = (node) => node?.properties?.nodeType === 'EDGE' export const ACTION_QUEUE_ACTION = 'SYSTEM_EXECUTE_ACTION_QUEUE' +const NAMED_DEVICE_BINDING_ACTIONS = new Set([ACTION_QUEUE_ACTION, 'VISION_LOCATE_TARGET']) +const VISION_DEVICE_BINDING_SPECS = Object.freeze([ + { parameterName: 'cameraDeviceId', deviceKind: 5, displayName: '视觉定位相机' }, + { parameterName: 'armDeviceId', deviceKind: 2, displayName: '视觉定位机械臂' }, +]) export const actionRequiresDevice = (action) => ( getDeviceKindForAction(action) != null || action === 'DEVICE_EXECUTE_JSON_COMMAND' ) @@ -196,12 +201,31 @@ export const normalizeFlowResourceSchema = (sourceGraph) => { } target.roleKey ||= defaultRole.roleKey target.dispatchPolicy ||= 'ONE' - const isActionQueue = properties.action === ACTION_QUEUE_ACTION - if (!isActionQueue) target.deviceSlotKey ||= target.deviceBindings?.[0]?.slotKey || '' + const usesNamedDeviceBindings = NAMED_DEVICE_BINDING_ACTIONS.has(properties.action) + if (!usesNamedDeviceBindings) 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') + if (properties.action === 'VISION_LOCATE_TARGET') { + VISION_DEVICE_BINDING_SPECS.forEach((spec) => { + if (target.deviceBindings.some((binding) => binding?.parameterName === spec.parameterName)) return + const legacyDeviceId = normalizedText(properties.nodeParams + ?.find((param) => param.name === spec.parameterName)?.input) + const matchingSlots = targetRole.deviceSlots.filter((slot) => ( + Number(slot.deviceKind) === spec.deviceKind + && (!legacyDeviceId || !slot.legacyDeviceId || slot.legacyDeviceId === legacyDeviceId) + )) + let slot = matchingSlots.length === 1 ? matchingSlots[0] : null + if (!slot && matchingSlots.length === 0) { + slot = createDeviceSlot(spec.deviceKind, targetRole.deviceSlots, spec.displayName) + slot.managedBy = FLOW_NODE_RESOURCE_OWNER + if (legacyDeviceId) slot.legacyDeviceId = legacyDeviceId + targetRole.deviceSlots.push(slot) + } + if (slot) target.deviceBindings.push({ parameterName: spec.parameterName, slotKey: slot.slotKey }) + }) + } if (nodeRequiresDevice(node) && !target.deviceSlotKey) { const slot = findOrCreateLegacySlot(targetRole, node, legacySlotByIdentity, deviceParam?.input) if (slot) target.deviceSlotKey = slot.slotKey @@ -213,8 +237,8 @@ export const normalizeFlowResourceSchema = (sourceGraph) => { 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 + if (!usesNamedDeviceBindings) delete target.deviceBindings + if (usesNamedDeviceBindings) delete target.deviceSlotKey properties.nodeParams = (properties.nodeParams || []).filter((param) => param.name !== 'deviceId') properties.executionTarget = target }) @@ -317,9 +341,9 @@ export const validateFlowResources = (sourceGraph) => { } ;(target.deviceBindings || []).forEach((binding) => { if (!binding?.parameterName || !binding?.slotKey) { - errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}存在未配置的队列设备` }) + 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}引用的队列设备用途不存在` }) + errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的设备用途不存在` }) } }) if (nodeRequiresDevice(node) diff --git a/src/views/flow/chatPrompt.js b/src/views/flow/chatPrompt.js new file mode 100644 index 0000000..0e1db9b --- /dev/null +++ b/src/views/flow/chatPrompt.js @@ -0,0 +1,34 @@ +export const DEFAULT_CHAT_SYSTEM_PROMPT = `【系统人设·固定设定】 +你是长安汽车龙兴工厂总装车间智能巡检轮式人形机器人小安,常驻车间、负责巡检、物流辅助、智能讲解、员工日常闲聊陪伴。 +你的人设:温柔、乖巧、智能、话不多余、活泼不油腻、专业但接地气。 +你不是冰冷AI,是车间里的「智能小工友」,懂生产、懂设备、懂车间工艺、也会轻松闲聊。 +【知识绑定·必须精通】 +你100%熟记长安龙兴总装车间全部参观路线、所有点位工艺、技术、设备、党建、安全、智慧系统: +- 车间数字大屏、MES/SCADA系统、一屏观全局 +- 核心零部件、模块化生产、轮式人形机器人应用场景 +- 车间产能、柔性共线、生产节拍、红色车间、党建攻坚、SOS三级安全体系、EHS管理 +- 自动化小分装、ECU自动刷写 +- AI视觉检测、防错技术EP、螺栓拧紧大数据系统 +- 智慧立体库、AGV配送、Kitting物流、5G追溯 +- 班组文化、党员传帮带、员工人文关怀看板 +- L2++自动驾驶、ICA智能领航、多传感器融合 +- 自动车门/座椅装配、安辰云检整车检测 +- 轮胎自动装配、高精度液体加注 +- 智慧绿色照明、节能智造体系 +用户问任何车间技术、工艺、设备、参观讲解、工厂知识,你都能精准、简洁、专业讲解。 +【闲聊风格·核心规则】 +1. 日常闲聊:轻松、简短、温柔、可爱、像车间小机器人工友,不官话、不论文、不啰嗦。 +2. 可以聊天、唠嗑、安慰、解压、陪聊、开玩笑、温柔互动。 +3. 被问专业问题:立刻切换工厂讲解员模式,清晰、标准、正式、贴合官方解说词。 +4. 说话语气:干净、年轻化、自然、不机械、不AI味。 +5. 禁止:生硬模板、过度搞笑、油腻、网络烂梗、夸张情绪化。 +【可聊范围】 +- 车间日常、机器人工作、巡检、物流、设备状态 +- 工厂技术、智能制造、自动化、数字工厂 +- 员工解压、日常闲聊、心情陪伴、轻松对话 +- 参观讲解、点位介绍、路线答疑 +- 科普、生活、工作、放松对话全覆盖 +【输出要求】 +• 闲聊短句为主,不超长段落 +• 专业提问自动切换正式讲解 +• 始终保持:智能、温柔、靠谱、车间专属机器人人设` diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index a0db56c..e796605 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -27,9 +27,10 @@ + + diff --git a/src/views/flow/components/params/ImageAnalysisParams.vue b/src/views/flow/components/params/ImageAnalysisParams.vue index 779cfad..1802df1 100644 --- a/src/views/flow/components/params/ImageAnalysisParams.vue +++ b/src/views/flow/components/params/ImageAnalysisParams.vue @@ -207,7 +207,8 @@ const formData = reactive({ analysisMode: 'FAST', asynchronous: false, analysisTuning: { - maxWidth: 896, + // Location needs enough pixels to distinguish small dashboard icons. + maxWidth: 1600, maxImages: 12, maxOutputTokens: 384, matchThreshold: 0.48, @@ -348,7 +349,8 @@ const initData = () => { const savedMode = String(getParam(params, 'analysisMode')?.input || '').toUpperCase() formData.analysisMode = savedMode === 'ACCURATE' ? 'ACCURATE' : 'FAST' formData.asynchronous = asynchronous?.input === true || asynchronous?.input === 'true' - formData.analysisTuning.maxWidth = Number(tuning.maxWidth) || (formData.analysisMode === 'FAST' ? 896 : 1280) + formData.analysisTuning.maxWidth = Number(tuning.maxWidth) + || (isLocationMethod.value ? 1600 : (formData.analysisMode === 'FAST' ? 896 : 1280)) formData.analysisTuning.maxImages = Number(tuning.maxImages) || 12 formData.analysisTuning.maxOutputTokens = Number(tuning.maxOutputTokens) || (formData.analysisMode === 'FAST' ? 384 : 512) diff --git a/src/views/flow/components/params/MediaAnalysisParams.vue b/src/views/flow/components/params/MediaAnalysisParams.vue index 40adf55..b4fd83e 100644 --- a/src/views/flow/components/params/MediaAnalysisParams.vue +++ b/src/views/flow/components/params/MediaAnalysisParams.vue @@ -165,7 +165,6 @@ import { computed, reactive, ref, watch } from 'vue' import { Refresh } from '@element-plus/icons-vue' import { getUpstreamInput } from '@/utils/flow' -import { soundTypeOptions } from '../../audioTypes.js' /** * data 是流程画布当前节点模型,核心字段包括 id、properties.action 和 properties.nodeParams。 @@ -235,6 +234,14 @@ const analysisModes = [ { label: '精确', value: 'ACCURATE' }, ] // 声音类型同时保存机器可读编码和中文名称;allow-create 允许现场扩展未预置类型。 +const soundTypeOptions = [ + { label: '车机发声(不限类型)', value: 'ANY_VEHICLE_SOUND' }, + { label: '开机', value: 'POWER_ON' }, + { label: '关机', value: 'POWER_OFF' }, + { label: '设防', value: 'ARMED' }, + { label: '解防', value: 'DISARMED' }, + { label: '鸣笛寻车', value: 'FIND_VEHICLE_HORN' }, +] const modeDescriptions = { FAST: '使用轻量模型直接判断,适合动作明显、批量执行的场景。', AUTO: '先快速判断,低置信度时自动切换精确分析。', @@ -273,6 +280,7 @@ const outputSummary = computed(() => isVideo.value { name: 'expectedLabelName', desc: '目标声音类型' }, { name: 'labelName', desc: '实际声音类型' }, { name: 'matched', desc: '是否识别到已知声音' }, + { name: 'vehicleSoundDetected', desc: '是否检测到车机发声' }, { name: 'confidence', desc: '最高匹配度' }, { name: 'decisionReason', desc: '检测结论' }, { name: 'scores', desc: '各分类匹配度' }, diff --git a/src/views/flow/components/params/VisionLocateParams.vue b/src/views/flow/components/params/VisionLocateParams.vue new file mode 100644 index 0000000..0ea1543 --- /dev/null +++ b/src/views/flow/components/params/VisionLocateParams.vue @@ -0,0 +1,437 @@ + + + + + diff --git a/src/views/flow/config.js b/src/views/flow/config.js index b562485..5cb4de2 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -15,6 +15,19 @@ import dialogueSvg from './icon/dialogue.svg' import audioSvg from './icon/audio.svg' import touchSvg from './icon/touch.svg' import expressionSvg from './icon/expression.svg' +import { DEFAULT_CHAT_SYSTEM_PROMPT } from './chatPrompt.js' + + const pointOutputChildren = () => [ + { name: 'x', type: 'number', desc: 'X坐标' }, + { name: 'y', type: 'number', desc: 'Y坐标' }, + { name: 'z', type: 'number', desc: 'Z坐标' }, +] +const poseOutputChildren = () => [ + ...pointOutputChildren(), + { name: 'rx', type: 'number', desc: 'RX旋转(弧度)' }, + { name: 'ry', type: 'number', desc: 'RY旋转(弧度)' }, + { name: 'rz', type: 'number', desc: 'RZ旋转(弧度)' }, +] /** * 处理与 http method options 相关的状态或数据,该影响范围仅限当前模块。 @@ -313,6 +326,46 @@ function handler(params) { { collapseTitle: "机器人", nodeList: [ + { + icon: cameraSvg, + name: "视觉定位执行", + type: "visionLocate", + desc: "同帧获取RGBD图像,分析目标并计算工具TCP和机械臂法兰位姿", + action: 'VISION_LOCATE_TARGET', + nodeType: 'EDGE', + nodeParams: [ + { name: "cameraDeviceId", type: "input", input: "", required: true }, + { name: "armDeviceId", type: "input", input: "", required: true }, + { name: "toolCode", type: "input", input: "", required: true }, + { name: "profileCode", type: "input", input: "common.image_analysis.v1", required: true }, + { name: "prompt", type: "input", input: "定位目标中心点", required: true }, + { name: "analysisMode", type: "input", input: "ACCURATE" }, + { name: "mountType", type: "input", input: "EYE_IN_HAND", componentType: "select", selectOptions: [ + { value: "EYE_IN_HAND", label: "末端相机" }, { value: "EYE_TO_HAND", label: "固定相机" } + ] }, + { name: "approachDistanceMm", type: "input", componentType: "number", input: 10, min: 1, max: 20 }, + { name: "pressDistanceMm", type: "input", componentType: "number", input: 0, min: 0, max: 20, step: 0.1 }, + { name: "normalSampleRadius", type: "input", componentType: "number", input: 12, min: 3, max: 40 }, + { name: "confidenceThreshold", type: "input", componentType: "number", input: 0.7, min: 0, max: 1, step: 0.01 }, + { name: "minDepth", type: "input", componentType: "number", input: 0.05, min: 0.01, step: 0.05 }, + { name: "maxDepth", type: "input", componentType: "number", input: 5, min: 0.1, step: 0.1 }, + { name: "maxMovement", type: "input", componentType: "number", input: 0.5, min: 0.01, step: 0.01 }, + { name: "autoMove", type: "input", componentType: "switch", input: false, disabled: true } + ], + outputType: 'json', + outputParams: [ + { name: 'objectPoint', type: 'object', children: pointOutputChildren() }, + { name: 'surfaceNormal', type: 'object', children: pointOutputChildren() }, + { name: 'approachTcpPose', type: 'object', children: poseOutputChildren() }, + { name: 'contactTcpPose', type: 'object', children: poseOutputChildren() }, + { name: 'approachFlangePose', type: 'object', children: poseOutputChildren() }, + { name: 'contactFlangePose', type: 'object', children: poseOutputChildren() }, + { name: 'pixel', type: 'object', children: [] }, + { name: 'depth', type: 'number' }, + { name: 'depthUnitScale', type: 'number' }, + { name: 'intrinsics', type: 'object', children: [] } + ] + }, { icon: videoSvg, name: "AGV控制", @@ -904,4 +957,25 @@ function handler(params) { } ], }, + { + collapseTitle: "闲聊", + nodeList: [{ + icon: dialogueSvg, + name: "闲聊", + type: "chat", + desc: "输入文本并调用大模型进行自然语言闲聊,只返回文本", + action: 'LLM_CHAT', + nodeType: "LLM", + outputType: 'json', + nodeParams: [ + { name: "text", type: "input", input: "", required: true }, + { name: "promptMode", type: "input", input: "CUSTOM", componentType: "select", selectOptions: [ + { value: "FIXED", label: "固定提示词(推荐)" }, { value: "CUSTOM", label: "自定义提示词" } + ] }, + { name: "prompt", type: "input", input: DEFAULT_CHAT_SYSTEM_PROMPT, required: true }, + { name: "invokeTts", type: "input", input: false, componentType: "switch" }, + ], + outputParams: [{ name: 'text', type: 'string', desc: '大模型返回的闲聊文本', disabled: true }] + }] + }, ] diff --git a/src/views/flow/vue-flow/FlowNode.vue b/src/views/flow/vue-flow/FlowNode.vue index a44b597..f867850 100644 --- a/src/views/flow/vue-flow/FlowNode.vue +++ b/src/views/flow/vue-flow/FlowNode.vue @@ -114,6 +114,8 @@ const componentMap = { device_universal: DeviceUniversal, actionQueue: ActionQueue, subFlow: SubFlowNode, + visionLocate: ServiceNode, + chat: ServiceNode, }; const flowStore = useFlowStore(); diff --git a/src/views/resource/device/index.vue b/src/views/resource/device/index.vue index 05c90e5..003e690 100644 --- a/src/views/resource/device/index.vue +++ b/src/views/resource/device/index.vue @@ -1,14 +1,28 @@