lxl-dev #3
@ -8,6 +8,17 @@ export function getSafetyState(robotId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function restoreOperationalState(data) {
|
||||||
|
return request({
|
||||||
|
url: '/api/system/restoreOperationalState',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use restoreOperationalState.
|
||||||
|
*/
|
||||||
export function recoverSafetyState(data) {
|
export function recoverSafetyState(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/system/recoverSafetyState',
|
url: '/api/system/recoverSafetyState',
|
||||||
|
|||||||
@ -4,6 +4,10 @@ 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([
|
||||||
|
{ name: 'result', type: 'object', desc: '按提示词返回的结果', children: [], disabled: true },
|
||||||
|
{ name: 'resultText', type: 'string', desc: '模型返回的原始文本', disabled: true },
|
||||||
|
])
|
||||||
|
|
||||||
const DEVICE_KIND_META = Object.freeze({
|
const DEVICE_KIND_META = Object.freeze({
|
||||||
1: { key: 'agv', name: '移动底盘' },
|
1: { key: 'agv', name: '移动底盘' },
|
||||||
@ -98,6 +102,27 @@ 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
|
||||||
|
if (node.type === 'imageAnalysis') {
|
||||||
|
const properties = node.properties || (node.properties = {})
|
||||||
|
properties.action = 'IMAGE_ANALYZE'
|
||||||
|
properties.nodeType = 'LLM'
|
||||||
|
properties.outputType = 'json'
|
||||||
|
properties.outputParams = clone(IMAGE_ANALYSIS_OUTPUT_PARAMS)
|
||||||
|
}
|
||||||
|
if (node.properties?.action === 'VIDEO_ANALYZE') {
|
||||||
|
const outputParams = Array.isArray(node.properties.outputParams)
|
||||||
|
? node.properties.outputParams
|
||||||
|
: []
|
||||||
|
if (!outputParams.some((param) => param?.name === 'result')) {
|
||||||
|
outputParams.unshift({ name: 'result', type: 'string', desc: '分析结果', disabled: true })
|
||||||
|
}
|
||||||
|
if (!outputParams.some((param) => param?.name === 'detailedOutput')) {
|
||||||
|
outputParams.push({
|
||||||
|
name: 'detailedOutput', type: 'boolean', desc: '是否返回详细事件证据', disabled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
node.properties.outputParams = outputParams
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const start = getStartNode(graph)
|
const start = getStartNode(graph)
|
||||||
if (!start) return graph
|
if (!start) return graph
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import DeviceUniversal from './params/DeviceUniversal.vue'
|
|||||||
import SwitchNodeParams from './params/SwitchNodeParams.vue'
|
import SwitchNodeParams from './params/SwitchNodeParams.vue'
|
||||||
import ActionQueueParams from './params/ActionQueueParams.vue'
|
import ActionQueueParams from './params/ActionQueueParams.vue'
|
||||||
import MediaAnalysisParams from './params/MediaAnalysisParams.vue'
|
import MediaAnalysisParams from './params/MediaAnalysisParams.vue'
|
||||||
|
import ImageAnalysisParams from './params/ImageAnalysisParams.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
drawer: Boolean,
|
drawer: Boolean,
|
||||||
@ -68,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 === 'branch') return SwitchNodeParams
|
if (type === 'branch') return SwitchNodeParams
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@ -178,6 +178,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
|
||||||
|
const isImage = analysisType.includes('IMAGE') || profileCode.includes('image')
|
||||||
|
|
||||||
// 仅有媒体类型还不足以展示分析面板,至少要存在一个分析状态或业务结果字段。
|
// 仅有媒体类型还不足以展示分析面板,至少要存在一个分析状态或业务结果字段。
|
||||||
const hasResult = Boolean(
|
const hasResult = Boolean(
|
||||||
@ -188,11 +189,13 @@ const analysisOf = (log) => {
|
|||||||
|| output.labelName
|
|| output.labelName
|
||||||
|| output.summary
|
|| output.summary
|
||||||
|| output.conclusion
|
|| output.conclusion
|
||||||
|
|| output.resultText
|
||||||
|
|| output.result !== undefined
|
||||||
|| output.passed !== undefined
|
|| output.passed !== undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
// 非分析日志也缓存 null,避免之后每次渲染都重复执行同一套识别逻辑。
|
// 非分析日志也缓存 null,避免之后每次渲染都重复执行同一套识别逻辑。
|
||||||
if (!hasResult || (!isAudio && !isVideo)) {
|
if (!hasResult || (!isAudio && !isVideo && !isImage)) {
|
||||||
analysisCache.set(log, null)
|
analysisCache.set(log, null)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -223,9 +226,9 @@ const analysisOf = (log) => {
|
|||||||
// 它与已经结构化展示的分析字段一起从“详情”中移除,避免用户看到重复且难以理解的底层上下文。
|
// 它与已经结构化展示的分析字段一起从“详情”中移除,避免用户看到重复且难以理解的底层上下文。
|
||||||
;[
|
;[
|
||||||
'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs',
|
'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs',
|
||||||
'mediaUrl', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched',
|
'mediaUrl', 'mediaUrls', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched',
|
||||||
'confidence', 'passed', 'summary', 'conclusion', 'events', '_executionContext',
|
'confidence', 'passed', 'summary', 'conclusion', 'events', '_executionContext',
|
||||||
'expectedLabel', 'expectedLabelName', 'decisionReason',
|
'expectedLabel', 'expectedLabelName', 'decisionReason', 'resultText', 'result',
|
||||||
].forEach((key) => delete detailSource[key])
|
].forEach((key) => delete detailSource[key])
|
||||||
|
|
||||||
// 所有已知字段删除后还有剩余内容时,才生成详情 JSON,避免显示空对象。
|
// 所有已知字段删除后还有剩余内容时,才生成详情 JSON,避免显示空对象。
|
||||||
@ -233,7 +236,7 @@ const analysisOf = (log) => {
|
|||||||
|
|
||||||
// 统一生成展示模型,模板不需要了解各版本日志字段和状态枚举的差异。
|
// 统一生成展示模型,模板不需要了解各版本日志字段和状态枚举的差异。
|
||||||
const result = {
|
const result = {
|
||||||
title: isAudio ? '声音类型检测' : '视频分析',
|
title: isAudio ? '声音类型检测' : isImage ? '通用图片分析' : '视频分析',
|
||||||
statusLabel,
|
statusLabel,
|
||||||
|
|
||||||
// 分析失败或业务判断未通过使用红色;仅匹配失败使用警告色,其余使用成功色。
|
// 分析失败或业务判断未通过使用红色;仅匹配失败使用警告色,其余使用成功色。
|
||||||
@ -249,7 +252,10 @@ 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.decisionReason || output.summary || output.conclusion || '',
|
summary: output.summary || output.conclusion || output.resultText
|
||||||
|
|| (typeof output.result === 'string' ? output.result : '')
|
||||||
|
|| (/^[A-Z_]+$/.test(String(output.decisionReason || '')) ? '' : output.decisionReason)
|
||||||
|
|| '',
|
||||||
isAudio,
|
isAudio,
|
||||||
events: Array.isArray(output.events)
|
events: Array.isArray(output.events)
|
||||||
? output.events.slice(0, 20).map((event) => typeof event === 'string' ? event : JSON.stringify(event))
|
? output.events.slice(0, 20).map((event) => typeof event === 'string' ? event : JSON.stringify(event))
|
||||||
@ -289,6 +295,10 @@ const mediaOf = (log) => {
|
|||||||
// mediaUrl 本身没有类型,必须结合分析类型或 profileCode 后才能作为视频/音频加入。
|
// mediaUrl 本身没有类型,必须结合分析类型或 profileCode 后才能作为视频/音频加入。
|
||||||
if (analysisType.includes('VIDEO') || profileCode.includes('video')) addMedia(result, seen, 'video', output.mediaUrl)
|
if (analysisType.includes('VIDEO') || profileCode.includes('video')) addMedia(result, seen, 'video', output.mediaUrl)
|
||||||
if (analysisType.includes('AUDIO') || profileCode.includes('audio')) addMedia(result, seen, 'audio', output.mediaUrl)
|
if (analysisType.includes('AUDIO') || profileCode.includes('audio')) addMedia(result, seen, 'audio', output.mediaUrl)
|
||||||
|
if (analysisType.includes('IMAGE') || profileCode.includes('image')) {
|
||||||
|
addMedia(result, seen, 'image', output.mediaUrls)
|
||||||
|
addMedia(result, seen, 'image', output.mediaUrl)
|
||||||
|
}
|
||||||
addMedia(result, seen, 'image', log.screenshotUrl)
|
addMedia(result, seen, 'image', log.screenshotUrl)
|
||||||
addMedia(result, seen, 'video', log.videoUrl)
|
addMedia(result, seen, 'video', log.videoUrl)
|
||||||
|
|
||||||
|
|||||||
274
src/views/flow/components/params/ImageAnalysisParams.vue
Normal file
274
src/views/flow/components/params/ImageAnalysisParams.vue
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-analysis-params">
|
||||||
|
<div class="intro">
|
||||||
|
<div class="intro__title">通用图片分析</div>
|
||||||
|
<div class="intro__desc">根据提示词分析一张或多张图片,模型结果保持提示词要求的内容和格式。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="formRef" :model="formData" :rules="rules" label-position="top">
|
||||||
|
<el-form-item label="图片来源" prop="imageMode">
|
||||||
|
<el-segmented v-model="formData.imageMode" :options="sourceOptions" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="formData.imageMode === 'quote'" label="引用上游图片" prop="imageQuote">
|
||||||
|
<el-cascader
|
||||||
|
ref="imageCascaderRef"
|
||||||
|
v-model="formData.imageQuote"
|
||||||
|
:options="quoteOptions"
|
||||||
|
:props="{ checkStrictly: true }"
|
||||||
|
placeholder="选择图片节点的 imageUrl 输出"
|
||||||
|
style="width: 100%"
|
||||||
|
@visible-change="loadQuoteOptions"
|
||||||
|
@change="handleQuoteChange"
|
||||||
|
/>
|
||||||
|
<div class="field-hint">上游输出为图片数组时,将按原顺序全部分析,最多 12 张。</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-else label="图片地址" prop="imageUrlsText">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.imageUrlsText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="每行一个 HTTP/HTTPS 图片地址,最多 12 张"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="分析提示词" prop="prompt">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.prompt"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
maxlength="4000"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="例如:读取图片中的仪表盘数值,只输出数字。或:识别所有仪表,按 JSON 输出名称、数值和单位。"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="分析模式">
|
||||||
|
<el-segmented v-model="formData.analysisMode" :options="modeOptions" block />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-collapse v-model="advancedSections" class="advanced-settings">
|
||||||
|
<el-collapse-item title="参考图片(可选)" name="reference">
|
||||||
|
<el-form-item label="参考图片来源">
|
||||||
|
<el-segmented v-model="formData.referenceMode" :options="sourceOptions" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="formData.referenceMode === 'quote'" label="引用参考图片" prop="referenceQuote">
|
||||||
|
<el-cascader
|
||||||
|
ref="referenceCascaderRef"
|
||||||
|
v-model="formData.referenceQuote"
|
||||||
|
:options="quoteOptions"
|
||||||
|
:props="{ checkStrictly: true }"
|
||||||
|
placeholder="选择参考图片输出"
|
||||||
|
style="width: 100%"
|
||||||
|
clearable
|
||||||
|
@visible-change="loadQuoteOptions"
|
||||||
|
@change="handleReferenceQuoteChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else label="参考图片地址" prop="referenceImageUrl">
|
||||||
|
<el-input v-model="formData.referenceImageUrl" clearable placeholder="HTTP/HTTPS 图片地址" />
|
||||||
|
</el-form-item>
|
||||||
|
<div class="field-hint">参考图片会作为最后一张图片传入;请在提示词中说明它的用途。</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="高级设置" name="advanced">
|
||||||
|
<el-form-item label="图片最大宽度">
|
||||||
|
<el-slider v-model="formData.analysisTuning.maxWidth" :min="640" :max="1600" :step="64" show-input />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最多分析图片">
|
||||||
|
<el-input-number v-model="formData.analysisTuning.maxImages" :min="1" :max="12" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最大输出长度">
|
||||||
|
<el-input-number v-model="formData.analysisTuning.maxOutputTokens" :min="32" :max="4096" :step="32" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-divider content-position="left">节点输出</el-divider>
|
||||||
|
<div class="output-list">
|
||||||
|
<div v-for="item in outputSummary" :key="item.name" class="output-item">
|
||||||
|
<code>{{ item.name }}</code><span>{{ item.desc }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import { getInput } from '@/utils/flow'
|
||||||
|
import { IMAGE_ANALYSIS_OUTPUT_PARAMS } from '@/utils/flowResources'
|
||||||
|
|
||||||
|
const props = defineProps({ data: Object })
|
||||||
|
const emit = defineEmits(['save-success', 'save-error'])
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const imageCascaderRef = ref()
|
||||||
|
const referenceCascaderRef = ref()
|
||||||
|
const quoteOptions = ref([])
|
||||||
|
const advancedSections = ref([])
|
||||||
|
|
||||||
|
const sourceOptions = [
|
||||||
|
{ label: '引用节点输出', value: 'quote' },
|
||||||
|
{ label: '输入图片地址', value: 'input' },
|
||||||
|
]
|
||||||
|
const modeOptions = [
|
||||||
|
{ label: '快速', value: 'FAST' },
|
||||||
|
{ label: '精准', value: 'ACCURATE' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
imageMode: 'quote',
|
||||||
|
imageQuote: [],
|
||||||
|
imageQuoteType: 'output',
|
||||||
|
imageUrlsText: '',
|
||||||
|
referenceMode: 'input',
|
||||||
|
referenceQuote: [],
|
||||||
|
referenceQuoteType: 'output',
|
||||||
|
referenceImageUrl: '',
|
||||||
|
prompt: '',
|
||||||
|
analysisMode: 'FAST',
|
||||||
|
analysisTuning: { maxWidth: 896, maxImages: 12, maxOutputTokens: 384 },
|
||||||
|
})
|
||||||
|
|
||||||
|
const outputSummary = [
|
||||||
|
{ name: 'result', desc: '按提示词返回的结果;JSON、数组和数字会自动解析' },
|
||||||
|
{ name: 'resultText', desc: '模型返回的原始文本' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const parseUrls = value => String(value || '').split(/\r?\n|,/).map(item => item.trim()).filter(Boolean)
|
||||||
|
const validUrl = value => /^https?:\/\//i.test(value)
|
||||||
|
const hasReference = () => formData.referenceMode === 'quote'
|
||||||
|
? formData.referenceQuote.length > 0
|
||||||
|
: Boolean(formData.referenceImageUrl.trim())
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
imageQuote: [{
|
||||||
|
validator: (_, value) => formData.imageMode !== 'quote' || value?.length
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error('请选择图片输出')),
|
||||||
|
trigger: 'change',
|
||||||
|
}],
|
||||||
|
imageUrlsText: [{
|
||||||
|
validator: (_, value) => {
|
||||||
|
if (formData.imageMode !== 'input') return Promise.resolve()
|
||||||
|
const urls = parseUrls(value)
|
||||||
|
if (!urls.length) return Promise.reject(new Error('请至少输入一个图片地址'))
|
||||||
|
if (urls.length > 12) return Promise.reject(new Error('最多输入 12 张图片'))
|
||||||
|
return urls.every(validUrl)
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error('图片地址必须是 HTTP/HTTPS 地址'))
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
}],
|
||||||
|
referenceImageUrl: [{
|
||||||
|
validator: (_, value) => !value || formData.referenceMode !== 'input' || validUrl(String(value).trim())
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error('请输入有效的参考图片地址')),
|
||||||
|
trigger: 'blur',
|
||||||
|
}],
|
||||||
|
prompt: [{ required: true, message: '请输入分析提示词', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadQuoteOptions = visible => { if (visible) quoteOptions.value = getInput(props.data.id) || [] }
|
||||||
|
const handleQuoteChange = () => {
|
||||||
|
formData.imageQuoteType = imageCascaderRef.value?.getCheckedNodes?.(true)?.[0]?.data?.type || 'output'
|
||||||
|
}
|
||||||
|
const handleReferenceQuoteChange = () => {
|
||||||
|
formData.referenceQuoteType = referenceCascaderRef.value?.getCheckedNodes?.(true)?.[0]?.data?.type || 'output'
|
||||||
|
}
|
||||||
|
const getParam = (params, name) => params.find(item => item.name === name)
|
||||||
|
|
||||||
|
const legacyPrompt = (params) => {
|
||||||
|
const target = String(getParam(params, 'targetDescription')?.input || '').trim()
|
||||||
|
const instruction = String(getParam(params, 'instruction')?.input || '').trim()
|
||||||
|
const taskType = String(getParam(params, 'taskType')?.input || '').toUpperCase()
|
||||||
|
const parts = [target || instruction]
|
||||||
|
if (taskType === 'ICON_TEMPLATE_LOCATE') {
|
||||||
|
parts.push('最后一张图片是参考图片,请在其他图片中查找对应目标并返回坐标。')
|
||||||
|
}
|
||||||
|
parts.push('只返回分析结果,不输出通过、未通过或置信度。')
|
||||||
|
return parts.filter(Boolean).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const initData = () => {
|
||||||
|
const params = props.data?.properties?.nodeParams || []
|
||||||
|
const images = getParam(params, 'imageUrl')
|
||||||
|
const reference = getParam(params, 'referenceImageUrl')
|
||||||
|
const tuning = getParam(params, 'analysisTuning')?.input || {}
|
||||||
|
formData.imageMode = images?.type === 'input' ? 'input' : 'quote'
|
||||||
|
formData.imageQuote = Array.isArray(images?.quote) ? [...images.quote] : []
|
||||||
|
formData.imageQuoteType = images?.quoteType || 'output'
|
||||||
|
formData.imageUrlsText = Array.isArray(images?.input) ? images.input.join('\n') : (images?.input || '')
|
||||||
|
formData.referenceMode = reference?.type === 'quote' ? 'quote' : 'input'
|
||||||
|
formData.referenceQuote = Array.isArray(reference?.quote) ? [...reference.quote] : []
|
||||||
|
formData.referenceQuoteType = reference?.quoteType || 'output'
|
||||||
|
formData.referenceImageUrl = reference?.input || ''
|
||||||
|
formData.prompt = getParam(params, 'prompt')?.input || legacyPrompt(params)
|
||||||
|
const savedMode = String(getParam(params, 'analysisMode')?.input || '').toUpperCase()
|
||||||
|
formData.analysisMode = savedMode === 'ACCURATE' ? 'ACCURATE' : 'FAST'
|
||||||
|
formData.analysisTuning.maxWidth = Number(tuning.maxWidth) || (formData.analysisMode === 'FAST' ? 896 : 1280)
|
||||||
|
formData.analysisTuning.maxImages = Number(tuning.maxImages) || 12
|
||||||
|
formData.analysisTuning.maxOutputTokens = Number(tuning.maxOutputTokens)
|
||||||
|
|| (formData.analysisMode === 'FAST' ? 384 : 512)
|
||||||
|
advancedSections.value = hasReference() ? ['reference'] : []
|
||||||
|
}
|
||||||
|
watch(() => props.data, initData, { immediate: true, deep: true })
|
||||||
|
|
||||||
|
const buildMediaParam = (name, mode, input, quote, quoteType, required = true) => ({
|
||||||
|
name,
|
||||||
|
type: mode,
|
||||||
|
input: mode === 'input' ? input : '',
|
||||||
|
quote: mode === 'quote' ? [...quote] : '',
|
||||||
|
quoteType,
|
||||||
|
required,
|
||||||
|
disabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateAndSave = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
const nodeParams = [
|
||||||
|
{ name: 'profileCode', type: 'input', input: 'common.image_analysis.v1', required: true, disabled: true },
|
||||||
|
buildMediaParam('imageUrl', formData.imageMode, parseUrls(formData.imageUrlsText), formData.imageQuote, formData.imageQuoteType),
|
||||||
|
{ name: 'prompt', type: 'input', input: formData.prompt.trim(), required: true, disabled: true, componentType: 'textarea' },
|
||||||
|
{ name: 'analysisMode', type: 'input', input: formData.analysisMode, required: false, disabled: true, componentType: 'select', selectOptions: modeOptions },
|
||||||
|
{ name: 'analysisTuning', type: 'input', input: { ...formData.analysisTuning }, required: false, disabled: true },
|
||||||
|
]
|
||||||
|
if (hasReference()) {
|
||||||
|
nodeParams.splice(2, 0, buildMediaParam(
|
||||||
|
'referenceImageUrl', formData.referenceMode, formData.referenceImageUrl.trim(),
|
||||||
|
formData.referenceQuote, formData.referenceQuoteType, false,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
const properties = {
|
||||||
|
...props.data.properties,
|
||||||
|
name: '通用图片分析',
|
||||||
|
action: 'IMAGE_ANALYZE',
|
||||||
|
nodeType: 'LLM',
|
||||||
|
nodeParams,
|
||||||
|
outputType: 'json',
|
||||||
|
outputParams: JSON.parse(JSON.stringify(IMAGE_ANALYSIS_OUTPUT_PARAMS)),
|
||||||
|
}
|
||||||
|
delete properties.executionTarget
|
||||||
|
lf.setProperties(props.data.id, properties)
|
||||||
|
emit('save-success')
|
||||||
|
} catch {
|
||||||
|
emit('save-error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ validateAndSave })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.image-analysis-params { padding-top: 16px; }
|
||||||
|
.intro { margin-bottom: 22px; padding: 14px 16px; border-left: 3px solid var(--el-color-primary); background: var(--el-fill-color-light); }
|
||||||
|
.intro__title { color: var(--el-text-color-primary); font-weight: 650; }
|
||||||
|
.intro__desc, .field-hint { margin-top: 5px; color: var(--el-text-color-secondary); font-size: 12px; line-height: 18px; }
|
||||||
|
.advanced-settings { margin-bottom: 18px; }
|
||||||
|
.output-list { display: grid; gap: 8px; }
|
||||||
|
.output-item { display: flex; align-items: baseline; gap: 12px; color: var(--el-text-color-regular); font-size: 13px; }
|
||||||
|
.output-item code { min-width: 92px; color: var(--el-color-primary); }
|
||||||
|
</style>
|
||||||
@ -100,6 +100,16 @@
|
|||||||
<div class="field-hint">{{ modeDescription }}</div>
|
<div class="field-hint">{{ modeDescription }}</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="isVideo" label="输出内容">
|
||||||
|
<el-switch
|
||||||
|
v-model="formData.detailedOutput"
|
||||||
|
inline-prompt
|
||||||
|
active-text="详细"
|
||||||
|
inactive-text="精简"
|
||||||
|
/>
|
||||||
|
<div class="field-hint">精简结果响应更快;需要逐步事件和时间范围时再启用详细输出。</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
高级参数只对视频抽帧分析生效。默认折叠以降低普通用户的配置负担,isTuned 用于提示
|
高级参数只对视频抽帧分析生效。默认折叠以降低普通用户的配置负担,isTuned 用于提示
|
||||||
当前值已经偏离模式预设;“恢复默认”只重置当前模式的参数,不切换分析模式。
|
当前值已经偏离模式预设;“恢复默认”只重置当前模式的参数,不切换分析模式。
|
||||||
@ -189,6 +199,7 @@ const formData = reactive({
|
|||||||
mediaUrl: '',
|
mediaUrl: '',
|
||||||
instruction: '',
|
instruction: '',
|
||||||
analysisMode: 'AUTO',
|
analysisMode: 'AUTO',
|
||||||
|
detailedOutput: false,
|
||||||
analysisTuning: { ...analysisPresets.AUTO },
|
analysisTuning: { ...analysisPresets.AUTO },
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -245,6 +256,7 @@ const profileOptions = computed(() => isVideo.value
|
|||||||
*/
|
*/
|
||||||
const outputSummary = computed(() => isVideo.value
|
const outputSummary = computed(() => isVideo.value
|
||||||
? [
|
? [
|
||||||
|
{ name: 'result', desc: '分析结果' },
|
||||||
{ name: 'passed', desc: '是否通过' },
|
{ name: 'passed', desc: '是否通过' },
|
||||||
{ name: 'confidence', desc: '判定置信度' },
|
{ name: 'confidence', desc: '判定置信度' },
|
||||||
{ name: 'evidenceSufficient', desc: '证据是否充分' },
|
{ name: 'evidenceSufficient', desc: '证据是否充分' },
|
||||||
@ -253,6 +265,7 @@ const outputSummary = computed(() => isVideo.value
|
|||||||
{ name: 'events', desc: '关键事件列表' },
|
{ name: 'events', desc: '关键事件列表' },
|
||||||
{ name: 'analysisMode', desc: '实际使用的分析模式' },
|
{ name: 'analysisMode', desc: '实际使用的分析模式' },
|
||||||
{ name: 'fallback', desc: '是否切换到精确分析' },
|
{ name: 'fallback', desc: '是否切换到精确分析' },
|
||||||
|
{ name: 'detailedOutput', desc: '是否返回详细事件证据' },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ name: 'passed', desc: '是否检测到目标声音' },
|
{ name: 'passed', desc: '是否检测到目标声音' },
|
||||||
@ -379,6 +392,7 @@ const initData = () => {
|
|||||||
const media = params.find(item => item.name === mediaName.value)
|
const media = params.find(item => item.name === mediaName.value)
|
||||||
const instruction = params.find(item => item.name === 'instruction')
|
const instruction = params.find(item => item.name === 'instruction')
|
||||||
const analysisMode = params.find(item => item.name === 'analysisMode')
|
const analysisMode = params.find(item => item.name === 'analysisMode')
|
||||||
|
const detailedOutput = params.find(item => item.name === 'detailedOutput')
|
||||||
const analysisTuning = params.find(item => item.name === 'analysisTuning')
|
const analysisTuning = params.find(item => item.name === 'analysisTuning')
|
||||||
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
||||||
formData.expectedLabel = expectedLabel?.input || ''
|
formData.expectedLabel = expectedLabel?.input || ''
|
||||||
@ -392,6 +406,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'
|
||||||
const preset = analysisPresets[formData.analysisMode] || analysisPresets.AUTO
|
const preset = analysisPresets[formData.analysisMode] || analysisPresets.AUTO
|
||||||
Object.assign(formData.analysisTuning, normalizeStoredTuning(analysisTuning?.input, preset))
|
Object.assign(formData.analysisTuning, normalizeStoredTuning(analysisTuning?.input, preset))
|
||||||
if (formData.analysisMode !== 'AUTO') formData.analysisTuning.fallbackToAccurate = false
|
if (formData.analysisMode !== 'AUTO') formData.analysisTuning.fallbackToAccurate = false
|
||||||
@ -474,6 +489,14 @@ const validateAndSave = async () => {
|
|||||||
componentType: 'select',
|
componentType: 'select',
|
||||||
selectOptions: analysisModes,
|
selectOptions: analysisModes,
|
||||||
})
|
})
|
||||||
|
nodeParams.push({
|
||||||
|
name: 'detailedOutput',
|
||||||
|
type: 'input',
|
||||||
|
input: formData.detailedOutput,
|
||||||
|
required: false,
|
||||||
|
disabled: true,
|
||||||
|
componentType: 'switch',
|
||||||
|
})
|
||||||
nodeParams.push({
|
nodeParams.push({
|
||||||
name: 'analysisTuning',
|
name: 'analysisTuning',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
|
|||||||
@ -556,10 +556,12 @@ 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: "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 },
|
||||||
],
|
],
|
||||||
outputType: "json",
|
outputType: "json",
|
||||||
outputParams: [
|
outputParams: [
|
||||||
|
{ name: "result", type: "string", desc: "分析结果", disabled: true },
|
||||||
{ name: "passed", type: "boolean", desc: "是否通过", disabled: true },
|
{ name: "passed", type: "boolean", desc: "是否通过", disabled: true },
|
||||||
{ name: "confidence", type: "number", desc: "判定置信度", disabled: true },
|
{ name: "confidence", type: "number", desc: "判定置信度", disabled: true },
|
||||||
{ name: "evidenceSufficient", type: "boolean", desc: "证据是否充分", disabled: true },
|
{ name: "evidenceSufficient", type: "boolean", desc: "证据是否充分", disabled: true },
|
||||||
@ -569,6 +571,27 @@ function handler(params) {
|
|||||||
{ name: "events", type: "array<object>", desc: "关键事件", children: [], disabled: true },
|
{ name: "events", type: "array<object>", desc: "关键事件", children: [], disabled: true },
|
||||||
{ name: "analysisMode", type: "string", desc: "实际分析模式", disabled: true },
|
{ name: "analysisMode", type: "string", desc: "实际分析模式", disabled: true },
|
||||||
{ name: "fallback", type: "boolean", desc: "是否切换精确分析", disabled: true },
|
{ name: "fallback", type: "boolean", desc: "是否切换精确分析", disabled: true },
|
||||||
|
{ name: "detailedOutput", type: "boolean", desc: "是否返回详细事件证据", disabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: cameraSvg,
|
||||||
|
name: "通用图片分析",
|
||||||
|
type: "imageAnalysis",
|
||||||
|
desc: "按照提示词分析一张或多张图片并返回指定内容",
|
||||||
|
action: 'IMAGE_ANALYZE',
|
||||||
|
nodeType: "LLM",
|
||||||
|
nodeParams: [
|
||||||
|
{ name: "profileCode", type: "input", input: "common.image_analysis.v1", required: true, disabled: true },
|
||||||
|
{ name: "imageUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
|
||||||
|
{ name: "prompt", type: "input", input: "", required: true, disabled: true, componentType: "textarea" },
|
||||||
|
{ name: "analysisMode", type: "input", input: "FAST", required: false, disabled: true },
|
||||||
|
{ name: "analysisTuning", type: "input", input: { maxWidth: 896, maxImages: 12, maxOutputTokens: 384 }, required: false, disabled: true },
|
||||||
|
],
|
||||||
|
outputType: "json",
|
||||||
|
outputParams: [
|
||||||
|
{ name: "result", type: "object", desc: "按提示词返回的结果", children: [], disabled: true },
|
||||||
|
{ name: "resultText", type: "string", desc: "模型返回的原始文本", disabled: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -24,9 +24,9 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
plain
|
plain
|
||||||
:loading="recoverSafetyLoading"
|
:loading="restoreOperationalLoading"
|
||||||
:disabled="flowState === 'testRunning'"
|
:disabled="flowState === 'testRunning'"
|
||||||
@click="openRecoverSafetyDialog"
|
@click="openRestoreOperationalDialog"
|
||||||
>
|
>
|
||||||
<el-icon><Warning /></el-icon>清除错误状态
|
<el-icon><Warning /></el-icon>清除错误状态
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -172,21 +172,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="recoverSafetyDialogVisible" title="清除错误状态" width="480" append-to-body>
|
<el-dialog v-model="restoreOperationalDialogVisible" title="恢复运行状态" width="480" append-to-body>
|
||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
<el-form-item label="机器人">
|
<el-form-item label="机器人">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="recoverSafetyRobotIds"
|
v-model="restoreOperationalRobotIds"
|
||||||
multiple
|
multiple
|
||||||
filterable
|
filterable
|
||||||
collapse-tags
|
collapse-tags
|
||||||
collapse-tags-tooltip
|
collapse-tags-tooltip
|
||||||
:loading="recoverRobotLoading"
|
:loading="restoreRobotLoading"
|
||||||
placeholder="请选择需要恢复的在线机器人"
|
placeholder="请选择需要恢复的在线机器人"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in recoverRobotOptions"
|
v-for="item in restoreRobotOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
@ -195,13 +195,13 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="recoverSafetyDialogVisible = false">取消</el-button>
|
<el-button @click="restoreOperationalDialogVisible = false">取消</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
:loading="recoverSafetyLoading"
|
:loading="restoreOperationalLoading"
|
||||||
:disabled="recoverSafetyRobotIds.length === 0"
|
:disabled="restoreOperationalRobotIds.length === 0"
|
||||||
@click="confirmRecoverSafetyState"
|
@click="confirmRestoreOperationalState"
|
||||||
>确认清除</el-button>
|
>恢复运行状态</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@ -249,7 +249,7 @@
|
|||||||
<el-input
|
<el-input
|
||||||
:disabled="field.param?.disabled || false"
|
:disabled="field.param?.disabled || false"
|
||||||
v-model="field.param.name"
|
v-model="field.param.name"
|
||||||
placeholder="请输入"
|
:placeholder="field.param.placeholder || '请输入'"
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -280,6 +280,13 @@
|
|||||||
:value="item.value"
|
:value="item.value"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-switch
|
||||||
|
v-model="field.param.input"
|
||||||
|
v-else-if="field.param.componentType === 'switch'"
|
||||||
|
inline-prompt
|
||||||
|
active-text="是"
|
||||||
|
inactive-text="否"
|
||||||
|
/>
|
||||||
<el-input
|
<el-input
|
||||||
v-else
|
v-else
|
||||||
v-model="field.param.input"
|
v-model="field.param.input"
|
||||||
@ -350,7 +357,7 @@ import {
|
|||||||
} from "@/api/flow/flow";
|
} from "@/api/flow/flow";
|
||||||
import { getDetect } from "@/api/test/detect";
|
import { getDetect } from "@/api/test/detect";
|
||||||
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||||
import { recoverSafetyState } from '@/api/device/system'
|
import { restoreOperationalState } from '@/api/device/system'
|
||||||
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
||||||
import {
|
import {
|
||||||
CircleClose,
|
CircleClose,
|
||||||
@ -377,53 +384,53 @@ const queueDropTargetId = ref("");
|
|||||||
const { onFlowEvent } = useFlowEvents();
|
const { onFlowEvent } = useFlowEvents();
|
||||||
window.lf = lf;
|
window.lf = lf;
|
||||||
|
|
||||||
const recoverSafetyDialogVisible = ref(false);
|
const restoreOperationalDialogVisible = ref(false);
|
||||||
const recoverRobotLoading = ref(false);
|
const restoreRobotLoading = ref(false);
|
||||||
const recoverSafetyLoading = ref(false);
|
const restoreOperationalLoading = ref(false);
|
||||||
const recoverRobotOptions = ref([]);
|
const restoreRobotOptions = ref([]);
|
||||||
const recoverSafetyRobotIds = ref([]);
|
const restoreOperationalRobotIds = ref([]);
|
||||||
|
|
||||||
const openRecoverSafetyDialog = async () => {
|
const openRestoreOperationalDialog = async () => {
|
||||||
recoverSafetyDialogVisible.value = true;
|
restoreOperationalDialogVisible.value = true;
|
||||||
recoverRobotLoading.value = true;
|
restoreRobotLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' });
|
const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' });
|
||||||
recoverRobotOptions.value = (response.rows || []).filter((robot) => robot.robotId).map((robot) => ({
|
restoreRobotOptions.value = (response.rows || []).filter((robot) => robot.robotId).map((robot) => ({
|
||||||
value: robot.robotId,
|
value: robot.robotId,
|
||||||
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
||||||
}));
|
}));
|
||||||
recoverSafetyRobotIds.value = recoverSafetyRobotIds.value.filter((robotId) => (
|
restoreOperationalRobotIds.value = restoreOperationalRobotIds.value.filter((robotId) => (
|
||||||
recoverRobotOptions.value.some((option) => option.value === robotId)
|
restoreRobotOptions.value.some((option) => option.value === robotId)
|
||||||
));
|
));
|
||||||
if (!recoverSafetyRobotIds.value.length && recoverRobotOptions.value.length === 1) {
|
if (!restoreOperationalRobotIds.value.length && restoreRobotOptions.value.length === 1) {
|
||||||
recoverSafetyRobotIds.value = [recoverRobotOptions.value[0].value];
|
restoreOperationalRobotIds.value = [restoreRobotOptions.value[0].value];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("加载在线机器人失败:", error);
|
console.error("加载在线机器人失败:", error);
|
||||||
recoverRobotOptions.value = [];
|
restoreRobotOptions.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
recoverRobotLoading.value = false;
|
restoreRobotLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmRecoverSafetyState = async () => {
|
const confirmRestoreOperationalState = async () => {
|
||||||
if (!recoverSafetyRobotIds.value.length) {
|
if (!restoreOperationalRobotIds.value.length) {
|
||||||
ElMessage.warning("请选择需要恢复的机器人");
|
ElMessage.warning("请选择需要恢复的机器人");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`将清除 ${recoverSafetyRobotIds.value.length} 台机器人的可恢复错误状态,确认继续?`,
|
`将恢复 ${restoreOperationalRobotIds.value.length} 台机器人的运行状态,确认继续?`,
|
||||||
"安全状态确认",
|
"安全状态确认",
|
||||||
{ type: "warning", confirmButtonText: "确认清除", cancelButtonText: "取消" },
|
{ type: "warning", confirmButtonText: "确认清除", cancelButtonText: "取消" },
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
recoverSafetyLoading.value = true;
|
restoreOperationalLoading.value = true;
|
||||||
const robotIds = [...recoverSafetyRobotIds.value];
|
const robotIds = [...restoreOperationalRobotIds.value];
|
||||||
try {
|
try {
|
||||||
const results = await Promise.allSettled(robotIds.map((robotId) => recoverSafetyState({
|
const results = await Promise.allSettled(robotIds.map((robotId) => restoreOperationalState({
|
||||||
robotId,
|
robotId,
|
||||||
reason: "工作流设计器手动清除错误状态",
|
reason: "工作流设计器手动清除错误状态",
|
||||||
timeoutMs: 15000,
|
timeoutMs: 15000,
|
||||||
@ -432,18 +439,18 @@ const confirmRecoverSafetyState = async () => {
|
|||||||
result.status === "rejected" ? [robotIds[index]] : []
|
result.status === "rejected" ? [robotIds[index]] : []
|
||||||
));
|
));
|
||||||
if (!failedRobotIds.length) {
|
if (!failedRobotIds.length) {
|
||||||
ElMessage.success(`已清除 ${robotIds.length} 台机器人的可恢复错误状态`);
|
ElMessage.success(`已恢复 ${robotIds.length} 台机器人的运行状态`);
|
||||||
recoverSafetyDialogVisible.value = false;
|
restoreOperationalDialogVisible.value = false;
|
||||||
recoverSafetyRobotIds.value = [];
|
restoreOperationalRobotIds.value = [];
|
||||||
} else {
|
} else {
|
||||||
const successCount = robotIds.length - failedRobotIds.length;
|
const successCount = robotIds.length - failedRobotIds.length;
|
||||||
recoverSafetyRobotIds.value = failedRobotIds;
|
restoreOperationalRobotIds.value = failedRobotIds;
|
||||||
ElMessage.error(successCount
|
ElMessage.error(successCount
|
||||||
? `${successCount} 台成功,${failedRobotIds.length} 台失败,请重试失败项`
|
? `${successCount} 台成功,${failedRobotIds.length} 台失败,请重试失败项`
|
||||||
: "清除错误状态失败,请检查机器人状态");
|
: "清除错误状态失败,请检查机器人状态");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
recoverSafetyLoading.value = false;
|
restoreOperationalLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1257,6 +1264,46 @@ const buildSingleNodePayload = (params) => {
|
|||||||
});
|
});
|
||||||
return payload;
|
return payload;
|
||||||
};
|
};
|
||||||
|
const normalizeImageAnalysisSingleNodeParams = (params) => {
|
||||||
|
const normalized = params || [];
|
||||||
|
const findValue = (name) => normalized.find((param) => param?.name === name)?.input;
|
||||||
|
if (!findValue('prompt')) {
|
||||||
|
const target = String(findValue('targetDescription') || '').trim();
|
||||||
|
const instruction = String(findValue('instruction') || '').trim();
|
||||||
|
const taskType = String(findValue('taskType') || '').toUpperCase();
|
||||||
|
const prompt = [target || instruction];
|
||||||
|
if (taskType === 'ICON_TEMPLATE_LOCATE') {
|
||||||
|
prompt.push('最后一张图片是参考图片,请在其他图片中查找对应目标并返回坐标。');
|
||||||
|
}
|
||||||
|
prompt.push('只返回分析结果,不输出通过、未通过或置信度。');
|
||||||
|
normalized.push({
|
||||||
|
name: 'prompt', type: 'input', input: prompt.filter(Boolean).join('\n'),
|
||||||
|
componentType: 'textarea', required: true, disabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let index = normalized.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (['taskType', 'targetDescription', 'instruction'].includes(normalized[index]?.name)) {
|
||||||
|
normalized.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalized.forEach((param) => {
|
||||||
|
if (!['imageUrl', 'referenceImageUrl'].includes(param?.name)) return;
|
||||||
|
param.type = 'input';
|
||||||
|
param.componentType = 'textarea';
|
||||||
|
param.input = Array.isArray(param.input) ? param.input.join('\n') : (param.input || '');
|
||||||
|
param.quote = '';
|
||||||
|
if (param.name === 'imageUrl') param.placeholder = '每行一个图片地址';
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
const buildImageAnalysisSingleNodePayload = (params) => {
|
||||||
|
const payload = buildSingleNodePayload(params);
|
||||||
|
payload.imageUrl = String(payload.imageUrl || '')
|
||||||
|
.split(/\r?\n|,/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
const validateSingleNodeParams = () => {
|
const validateSingleNodeParams = () => {
|
||||||
for (const { param } of singleNodeFields.value) {
|
for (const { param } of singleNodeFields.value) {
|
||||||
if (!String(param?.name || "").trim()) {
|
if (!String(param?.name || "").trim()) {
|
||||||
@ -1430,7 +1477,9 @@ const singleNodeExecution = async (e) => {
|
|||||||
await handleSingleNodeRobotChange(cachedRobotId);
|
await handleSingleNodeRobotChange(cachedRobotId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
formData.nodeParams = executionParams;
|
formData.nodeParams = normalizedAction === 'IMAGE_ANALYZE'
|
||||||
|
? normalizeImageAnalysisSingleNodeParams(executionParams)
|
||||||
|
: executionParams;
|
||||||
}
|
}
|
||||||
if (nodeAction.value === "AGV_MOVE_TO_STATION") {
|
if (nodeAction.value === "AGV_MOVE_TO_STATION") {
|
||||||
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
||||||
@ -1476,7 +1525,9 @@ const execute = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await flowAction({
|
const res = await flowAction({
|
||||||
action: nodeAction.value,
|
action: nodeAction.value,
|
||||||
payload: buildSingleNodePayload(formData.nodeParams),
|
payload: nodeAction.value === 'IMAGE_ANALYZE'
|
||||||
|
? buildImageAnalysisSingleNodePayload(formData.nodeParams)
|
||||||
|
: buildSingleNodePayload(formData.nodeParams),
|
||||||
});
|
});
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success("执行成功");
|
ElMessage.success("执行成功");
|
||||||
|
|||||||
@ -290,6 +290,8 @@ const analysisVerdictOf = (log) => {
|
|||||||
const testStatus = String(log?.testStatus ?? '')
|
const testStatus = String(log?.testStatus ?? '')
|
||||||
const analysisType = String(output.analysisType || '').toUpperCase()
|
const analysisType = String(output.analysisType || '').toUpperCase()
|
||||||
const profileCode = String(output.profileCode || '').toLowerCase()
|
const profileCode = String(output.profileCode || '').toLowerCase()
|
||||||
|
// 通用图片分析只返回提示词要求的内容,不承担检测项通过/不通过判定。
|
||||||
|
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')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user