Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev

# Conflicts:
#	src/utils/flowResources.js
#	src/views/flow/components/params/ImageAnalysisParams.vue
This commit is contained in:
lixiaolong 2026-08-20 11:47:37 +08:00
commit ad64917603
10 changed files with 58 additions and 2 deletions

View File

@ -8,6 +8,9 @@ export function getSafetyState(robotId) {
}) })
} }
/**
* 恢复机器人运行状态;payload 与旧 recoverSafetyState 保持一致,便于调用方渐进迁移。
*/
export function restoreOperationalState(data) { export function restoreOperationalState(data) {
return request({ return request({
url: '/api/system/restoreOperationalState', url: '/api/system/restoreOperationalState',
@ -17,7 +20,7 @@ export function restoreOperationalState(data) {
} }
/** /**
* @deprecated Use restoreOperationalState. * @deprecated 请改用 restoreOperationalState,旧接口仅保留给未迁移调用方。
*/ */
export function recoverSafetyState(data) { export function recoverSafetyState(data) {
return request({ return request({

View File

@ -4,6 +4,7 @@ export const FLOW_SCHEMA_VERSION = 2
export const DEFAULT_ROLE_KEY = 'default_executor' export const DEFAULT_ROLE_KEY = 'default_executor'
export const FLOW_NODE_RESOURCE_OWNER = 'FLOW_NODE' export const FLOW_NODE_RESOURCE_OWNER = 'FLOW_NODE'
export const USER_RESOURCE_OWNER = 'USER' export const USER_RESOURCE_OWNER = 'USER'
// 图片分析节点的输出契约会被节点配置、参数面板和旧流程迁移复用,集中定义避免字段漂移。
export const IMAGE_ANALYSIS_OUTPUT_PARAMS = Object.freeze([ export const IMAGE_ANALYSIS_OUTPUT_PARAMS = Object.freeze([
{ name: 'result', type: 'object', desc: '按提示词返回的结果', children: [], disabled: true }, { name: 'result', type: 'object', desc: '按提示词返回的结果', children: [], disabled: true },
{ name: 'resultText', type: 'string', desc: '模型返回的原始文本', disabled: true }, { name: 'resultText', type: 'string', desc: '模型返回的原始文本', disabled: true },
@ -111,6 +112,7 @@ export const normalizeFlowResourceSchema = (sourceGraph) => {
graph.edges = Array.isArray(graph.edges) ? graph.edges : [] graph.edges = Array.isArray(graph.edges) ? graph.edges : []
graph.nodes.forEach((node) => { graph.nodes.forEach((node) => {
node.type = LEGACY_NODE_TYPE_ALIASES[node.type] || node.type node.type = LEGACY_NODE_TYPE_ALIASES[node.type] || node.type
// 早期图片分析图数据可能只保存节点类型,加载时补齐运行端识别所需的协议字段。
const properties = node.properties || (node.properties = {}) const properties = node.properties || (node.properties = {})
if (['AGV_MOVE_TO_POINT', 'AGV_MOVE_TO_STATION'].includes(properties.action)) { if (['AGV_MOVE_TO_POINT', 'AGV_MOVE_TO_STATION'].includes(properties.action)) {
properties.nodeParams = Array.isArray(properties.nodeParams) ? properties.nodeParams : [] properties.nodeParams = Array.isArray(properties.nodeParams) ? properties.nodeParams : []
@ -127,11 +129,13 @@ export const normalizeFlowResourceSchema = (sourceGraph) => {
} }
} }
if (node.type === 'imageAnalysis') { if (node.type === 'imageAnalysis') {
const properties = node.properties || (node.properties = {})
properties.action = 'IMAGE_ANALYZE' properties.action = 'IMAGE_ANALYZE'
properties.nodeType = 'LLM' properties.nodeType = 'LLM'
properties.outputType = 'json' properties.outputType = 'json'
properties.outputParams = clone(IMAGE_ANALYSIS_OUTPUT_PARAMS) properties.outputParams = clone(IMAGE_ANALYSIS_OUTPUT_PARAMS)
} }
// 视频分析新增 result/detailedOutput 后,打开旧流程时补齐输出字段,后续节点才能稳定引用。
if (node.properties?.action === 'VIDEO_ANALYZE') { if (node.properties?.action === 'VIDEO_ANALYZE') {
const outputParams = Array.isArray(node.properties.outputParams) const outputParams = Array.isArray(node.properties.outputParams)
? node.properties.outputParams ? node.properties.outputParams

View File

@ -69,6 +69,7 @@ const currentComponent = computed(() => {
if (['deviceUniversal', 'device_universal'].includes(type)) return DeviceUniversal if (['deviceUniversal', 'device_universal'].includes(type)) return DeviceUniversal
if (type === 'actionQueue') return ActionQueueParams if (type === 'actionQueue') return ActionQueueParams
if (type === 'mediaAnalysis') return MediaAnalysisParams if (type === 'mediaAnalysis') return MediaAnalysisParams
// 图片分析支持多图、参考图和自由提示词输出,使用独立面板保持媒体分析面板聚焦音视频。
if (type === 'imageAnalysis') return ImageAnalysisParams if (type === 'imageAnalysis') return ImageAnalysisParams
if (type === 'branch') return SwitchNodeParams if (type === 'branch') return SwitchNodeParams
return null return null

View File

@ -205,6 +205,7 @@ const analysisOf = (log) => {
const profileCode = String(output.profileCode || '').toLowerCase() const profileCode = String(output.profileCode || '').toLowerCase()
const isAudio = analysisType.includes('AUDIO') || profileCode.includes('audio') || Number(log.logType) === 4 const isAudio = analysisType.includes('AUDIO') || profileCode.includes('audio') || Number(log.logType) === 4
const isVideo = analysisType.includes('VIDEO') || profileCode.includes('video') || Number(log.logType) === 3 const isVideo = analysisType.includes('VIDEO') || profileCode.includes('video') || Number(log.logType) === 3
// 图片分析没有历史 logType 枚举,只依赖新协议中的 analysisType/profileCode 识别。
const isImage = analysisType.includes('IMAGE') || profileCode.includes('image') const isImage = analysisType.includes('IMAGE') || profileCode.includes('image')
// 仅有媒体类型还不足以展示分析面板,至少要存在一个分析状态或业务结果字段。 // 仅有媒体类型还不足以展示分析面板,至少要存在一个分析状态或业务结果字段。
@ -279,6 +280,7 @@ const analysisOf = (log) => {
matchLabel: isAudio ? '' : matched === true ? '匹配' : matched === false ? '未匹配' : '', matchLabel: isAudio ? '' : matched === true ? '匹配' : matched === false ? '未匹配' : '',
confidence, confidence,
passedLabel: passed === true ? '通过' : passed === false ? '未通过' : '', passedLabel: passed === true ? '通过' : passed === false ? '未通过' : '',
// 图片分析由提示词决定输出形态,优先展示模型文本或字符串结果,避免把枚举型原因当摘要。
summary: output.summary || output.conclusion || output.resultText summary: output.summary || output.conclusion || output.resultText
|| (typeof output.result === 'string' ? output.result : '') || (typeof output.result === 'string' ? output.result : '')
|| (/^[A-Z_]+$/.test(String(output.decisionReason || '')) ? '' : output.decisionReason) || (/^[A-Z_]+$/.test(String(output.decisionReason || '')) ? '' : output.decisionReason)

View File

@ -155,7 +155,7 @@ const remember = () => {
const loadRobotOptions = async () => { const loadRobotOptions = async () => {
robotLoading.value = true robotLoading.value = true
try { try {
const response = await getRobotList({ pageNum: 1, pageSize: 1000 }) const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: 1 })
robotOptions.value = (response.rows || []).map((robot) => ({ robotOptions.value = (response.rows || []).map((robot) => ({
label: `${robot.robotName || robot.robotId} (${robot.robotId})`, label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
value: robot.robotId, value: robot.robotId,

View File

@ -1,3 +1,7 @@
<!--
* 文件说明:通用图片分析节点参数编辑器,支持引用上游图片或直接填写 HTTP/HTTPS 图片地址。
* 数据流向:从 nodeParams 回填表单,保存时写回 IMAGE_ANALYZE 执行协议和统一输出字段。
-->
<template> <template>
<div class="image-analysis-params"> <div class="image-analysis-params">
<div class="intro"> <div class="intro">
@ -159,6 +163,7 @@ const referenceCascaderRef = ref()
const quoteOptions = ref([]) const quoteOptions = ref([])
const advancedSections = ref([]) const advancedSections = ref([])
// 这里的 value 会直接写入 nodeParams.type,需与流程参数协议中的 quote/input 保持一致。
const sourceOptions = [ const sourceOptions = [
{ label: '引用节点输出', value: 'quote' }, { label: '引用节点输出', value: 'quote' },
{ label: '输入图片地址', value: 'input' }, { label: '输入图片地址', value: 'input' },
@ -185,6 +190,9 @@ const matchSortOptions = [
{ label: '先列后行', value: 'COLUMN_MAJOR' }, { label: '先列后行', value: 'COLUMN_MAJOR' },
] ]
/**
* 使用稳定的 reactive 对象承载表单状态;initData 只覆盖字段,避免 Element Plus 校验引用失效。
*/
const formData = reactive({ const formData = reactive({
analysisMethod: 'VISION_MODEL', analysisMethod: 'VISION_MODEL',
imageMode: 'quote', imageMode: 'quote',
@ -229,12 +237,16 @@ const outputSummary = [
{ name: 'selectedMatchIndex', desc: '当前选择的匹配序号' }, { name: 'selectedMatchIndex', desc: '当前选择的匹配序号' },
] ]
// 文本输入允许换行或逗号分隔,保存前统一转成执行端需要的 URL 数组。
const parseUrls = value => String(value || '').split(/\r?\n|,/).map(item => item.trim()).filter(Boolean) const parseUrls = value => String(value || '').split(/\r?\n|,/).map(item => item.trim()).filter(Boolean)
const validUrl = value => /^https?:\/\//i.test(value) const validUrl = value => /^https?:\/\//i.test(value)
const hasReference = () => formData.referenceMode === 'quote' const hasReference = () => formData.referenceMode === 'quote'
? formData.referenceQuote.length > 0 ? formData.referenceQuote.length > 0
: Boolean(formData.referenceImageUrl.trim()) : Boolean(formData.referenceImageUrl.trim())
/**
* 条件校验只约束当前可见的图片来源,隐藏模式不阻塞保存;直接输入仅接受 HTTP/HTTPS。
*/
const rules = { const rules = {
imageQuote: [{ imageQuote: [{
validator: (_, value) => formData.imageMode !== 'quote' || value?.length validator: (_, value) => formData.imageMode !== 'quote' || value?.length
@ -279,6 +291,7 @@ const rules = {
}], }],
} }
// 引用选项按需加载,上游流程较大时避免每次渲染都遍历画布。
const loadQuoteOptions = visible => { if (visible) quoteOptions.value = getInput(props.data.id) || [] } const loadQuoteOptions = visible => { if (visible) quoteOptions.value = getInput(props.data.id) || [] }
const handleQuoteChange = () => { const handleQuoteChange = () => {
formData.imageQuoteType = imageCascaderRef.value?.getCheckedNodes?.(true)?.[0]?.data?.type || 'output' formData.imageQuoteType = imageCascaderRef.value?.getCheckedNodes?.(true)?.[0]?.data?.type || 'output'
@ -288,6 +301,9 @@ const handleReferenceQuoteChange = () => {
} }
const getParam = (params, name) => params.find(item => item.name === name) const getParam = (params, name) => params.find(item => item.name === name)
/**
* 兼容旧版图片识别节点:把 taskType、targetDescription、instruction 合成新的自由提示词。
*/
const legacyPrompt = (params) => { const legacyPrompt = (params) => {
const target = String(getParam(params, 'targetDescription')?.input || '').trim() const target = String(getParam(params, 'targetDescription')?.input || '').trim()
const instruction = String(getParam(params, 'instruction')?.input || '').trim() const instruction = String(getParam(params, 'instruction')?.input || '').trim()
@ -300,6 +316,9 @@ const legacyPrompt = (params) => {
return parts.filter(Boolean).join('\n') return parts.filter(Boolean).join('\n')
} }
/**
* 将持久化 nodeParams 回填到表单,并为缺少新字段的旧节点补默认值。
*/
const initData = () => { const initData = () => {
const params = props.data?.properties?.nodeParams || [] const params = props.data?.properties?.nodeParams || []
const images = getParam(params, 'imageUrl') const images = getParam(params, 'imageUrl')
@ -343,6 +362,9 @@ const initData = () => {
} }
watch(() => props.data, initData, { immediate: true, deep: true }) watch(() => props.data, initData, { immediate: true, deep: true })
/**
* 媒体参数统一保存为 quote/input 二选一结构;未使用的一侧清空,避免执行端误读。
*/
const buildMediaParam = (name, mode, input, quote, quoteType, required = true) => ({ const buildMediaParam = (name, mode, input, quote, quoteType, required = true) => ({
name, name,
type: mode, type: mode,
@ -353,6 +375,9 @@ const buildMediaParam = (name, mode, input, quote, quoteType, required = true) =
disabled: true, disabled: true,
}) })
/**
* 校验通过后重建 IMAGE_ANALYZE 节点属性,确保 action、类型和输出契约同步升级。
*/
const validateAndSave = async () => { const validateAndSave = async () => {
try { try {
await formRef.value.validate() await formRef.value.validate()
@ -366,6 +391,7 @@ const validateAndSave = async () => {
{ name: 'asynchronous', type: 'input', input: formData.asynchronous, required: false, disabled: true, componentType: 'switch' }, { name: 'asynchronous', type: 'input', input: formData.asynchronous, required: false, disabled: true, componentType: 'switch' },
] ]
if (!isTextLocate.value && hasReference()) { if (!isTextLocate.value && hasReference()) {
// 参考图存在时插入到图片来源和提示词之间,保持执行端按固定顺序读取。
nodeParams.splice(3, 0, buildMediaParam( nodeParams.splice(3, 0, buildMediaParam(
'referenceImageUrl', formData.referenceMode, formData.referenceImageUrl.trim(), 'referenceImageUrl', formData.referenceMode, formData.referenceImageUrl.trim(),
formData.referenceQuote, formData.referenceQuoteType, false, formData.referenceQuote, formData.referenceQuoteType, false,
@ -380,6 +406,7 @@ const validateAndSave = async () => {
outputType: 'json', outputType: 'json',
outputParams: JSON.parse(JSON.stringify(IMAGE_ANALYSIS_OUTPUT_PARAMS)), outputParams: JSON.parse(JSON.stringify(IMAGE_ANALYSIS_OUTPUT_PARAMS)),
} }
// 图片分析由 LLM 服务执行,不绑定机器人或设备资源。
delete properties.executionTarget delete properties.executionTarget
lf.setProperties(props.data.id, properties) lf.setProperties(props.data.id, properties)
emit('save-success') emit('save-success')

View File

@ -106,6 +106,7 @@
</el-form-item> </el-form-item>
<el-form-item v-if="isVideo" label="输出内容"> <el-form-item v-if="isVideo" label="输出内容">
<!-- detailedOutput 只影响视频节点:默认精简,需要事件证据和时间范围时再让后端返回完整结果。 -->
<el-switch <el-switch
v-model="formData.detailedOutput" v-model="formData.detailedOutput"
inline-prompt inline-prompt
@ -413,6 +414,7 @@ const initData = () => {
formData.mediaUrl = media?.input || '' formData.mediaUrl = media?.input || ''
formData.instruction = instruction?.input || '' formData.instruction = instruction?.input || ''
formData.analysisMode = analysisPresets[analysisMode?.input] ? analysisMode.input : 'AUTO' formData.analysisMode = analysisPresets[analysisMode?.input] ? analysisMode.input : 'AUTO'
// 兼容旧流程或外部写入的字符串值,表单内部统一使用布尔值驱动开关。
formData.detailedOutput = detailedOutput?.input === true || detailedOutput?.input === 'true' formData.detailedOutput = detailedOutput?.input === true || detailedOutput?.input === 'true'
formData.asynchronous = asynchronous?.input === true || asynchronous?.input === 'true' formData.asynchronous = asynchronous?.input === true || asynchronous?.input === 'true'
const preset = analysisPresets[formData.analysisMode] || analysisPresets.AUTO const preset = analysisPresets[formData.analysisMode] || analysisPresets.AUTO
@ -505,6 +507,7 @@ const validateAndSave = async () => {
componentType: 'select', componentType: 'select',
selectOptions: analysisModes, selectOptions: analysisModes,
}) })
// 该开关控制后端是否返回 events 等较重字段,默认 false 可降低单节点执行响应成本。
nodeParams.push({ nodeParams.push({
name: 'detailedOutput', name: 'detailedOutput',
type: 'input', type: 'input',

View File

@ -559,6 +559,7 @@ function handler(params) {
{ name: "videoUrl", type: "quote", input: "", quote: "", required: true, disabled: true }, { name: "videoUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
{ name: "instruction", type: "input", input: "", required: true, disabled: true }, { name: "instruction", type: "input", input: "", required: true, disabled: true },
{ name: "analysisMode", type: "input", input: "AUTO", required: false, disabled: true }, { name: "analysisMode", type: "input", input: "AUTO", required: false, disabled: true },
// 默认返回精简结论;用户开启详细输出时,执行端才补充事件证据。
{ name: "detailedOutput", type: "input", input: false, required: false, disabled: true, componentType: "switch" }, { name: "detailedOutput", type: "input", input: false, required: false, disabled: true, componentType: "switch" },
{ name: "analysisTuning", type: "input", input: { sampleFps: 2, maxWidth: 960, confidenceThreshold: 0.75, fallbackToAccurate: true }, required: false, disabled: true }, { name: "analysisTuning", type: "input", input: { sampleFps: 2, maxWidth: 960, confidenceThreshold: 0.75, fallbackToAccurate: true }, required: false, disabled: true },
{ name: "asynchronous", type: "input", input: false, required: false, disabled: true, componentType: "switch" }, { name: "asynchronous", type: "input", input: false, required: false, disabled: true, componentType: "switch" },
@ -579,6 +580,7 @@ function handler(params) {
], ],
}, },
{ {
// 通用图片分析不内置通过/不通过判定,返回结构完全由用户提示词约定。
icon: cameraSvg, icon: cameraSvg,
name: "通用图片分析", name: "通用图片分析",
type: "imageAnalysis", type: "imageAnalysis",

View File

@ -384,6 +384,9 @@ const queueDropTargetId = ref("");
const { onFlowEvent } = useFlowEvents(); const { onFlowEvent } = useFlowEvents();
window.lf = lf; window.lf = lf;
/**
* 恢复运行状态弹窗只面向在线机器人;局部失败时保留失败项,方便用户直接重试。
*/
const restoreOperationalDialogVisible = ref(false); const restoreOperationalDialogVisible = ref(false);
const restoreRobotLoading = ref(false); const restoreRobotLoading = ref(false);
const restoreOperationalLoading = ref(false); const restoreOperationalLoading = ref(false);
@ -430,6 +433,7 @@ const confirmRestoreOperationalState = async () => {
restoreOperationalLoading.value = true; restoreOperationalLoading.value = true;
const robotIds = [...restoreOperationalRobotIds.value]; const robotIds = [...restoreOperationalRobotIds.value];
try { try {
// 并发恢复多台机器人,使用 allSettled 保留局部失败信息并提示用户重试失败项。
const results = await Promise.allSettled(robotIds.map((robotId) => restoreOperationalState({ const results = await Promise.allSettled(robotIds.map((robotId) => restoreOperationalState({
robotId, robotId,
reason: "工作流设计器手动清除错误状态", reason: "工作流设计器手动清除错误状态",
@ -1264,6 +1268,10 @@ const buildSingleNodePayload = (params) => {
}); });
return payload; return payload;
}; };
/**
* 单节点调试入口兼容旧版图片识别参数,把固定检测字段转换成 IMAGE_ANALYZE 的自由提示词协议。
*/
const normalizeImageAnalysisSingleNodeParams = (params) => { const normalizeImageAnalysisSingleNodeParams = (params) => {
const normalized = params || []; const normalized = params || [];
const findValue = (name) => normalized.find((param) => param?.name === name)?.input; const findValue = (name) => normalized.find((param) => param?.name === name)?.input;
@ -1287,6 +1295,7 @@ const normalizeImageAnalysisSingleNodeParams = (params) => {
} }
} }
normalized.forEach((param) => { normalized.forEach((param) => {
// 单节点弹窗无法从上游引用取值,图片参数统一改成可粘贴多行地址的输入框。
if (!['imageUrl', 'referenceImageUrl'].includes(param?.name)) return; if (!['imageUrl', 'referenceImageUrl'].includes(param?.name)) return;
param.type = 'input'; param.type = 'input';
param.componentType = 'textarea'; param.componentType = 'textarea';
@ -1296,6 +1305,10 @@ const normalizeImageAnalysisSingleNodeParams = (params) => {
}); });
return normalized; return normalized;
}; };
/**
* 执行端接收 imageUrl 数组;调试弹窗为了便于输入先用文本框承载,提交前再切回数组。
*/
const buildImageAnalysisSingleNodePayload = (params) => { const buildImageAnalysisSingleNodePayload = (params) => {
const payload = buildSingleNodePayload(params); const payload = buildSingleNodePayload(params);
payload.imageUrl = String(payload.imageUrl || '') payload.imageUrl = String(payload.imageUrl || '')

View File

@ -292,6 +292,7 @@ const analysisVerdictOf = (log) => {
const profileCode = String(output.profileCode || '').toLowerCase() const profileCode = String(output.profileCode || '').toLowerCase()
// 通用图片分析只返回提示词要求的内容,不承担检测项通过/不通过判定。 // 通用图片分析只返回提示词要求的内容,不承担检测项通过/不通过判定。
if (analysisType.includes('IMAGE') || profileCode.includes('image')) return null if (analysisType.includes('IMAGE') || profileCode.includes('image')) return null
// 其余展示判定仍限定在音频/视频分析结果内,避免普通执行日志误入检测结论。
const hasAnalysis = Boolean( const hasAnalysis = Boolean(
analysisType.includes('AUDIO') || analysisType.includes('VIDEO') analysisType.includes('AUDIO') || analysisType.includes('VIDEO')
|| profileCode.includes('audio') || profileCode.includes('video') || profileCode.includes('audio') || profileCode.includes('video')