lxl-dev #3
@ -57,6 +57,7 @@ const nodeToQueueStep = (node) => {
|
||||
y: numberValue(params.y),
|
||||
theta: numberValue(params.theta),
|
||||
maxSpeed: numberValue(params.maxSpeed, 0.5),
|
||||
blockedTimeoutMs: numberValue(params.blockedTimeoutMs, 60000),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -68,6 +69,7 @@ const nodeToQueueStep = (node) => {
|
||||
params: {
|
||||
stationId: params.stationId ?? '',
|
||||
maxSpeed: numberValue(params.maxSpeed, 0.5),
|
||||
blockedTimeoutMs: numberValue(params.blockedTimeoutMs, 60000),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,15 @@ 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 },
|
||||
{ name: 'matched', type: 'boolean', desc: '找图是否匹配', disabled: true },
|
||||
{ name: 'matchScore', type: 'number', desc: '匹配分数或定位置信度', disabled: true },
|
||||
{ name: 'coordinates', type: 'array<number>', desc: '原图坐标 [x1,y1,x2,y2]', children: [], disabled: true },
|
||||
{ name: 'center', type: 'array<number>', desc: '目标中心点 [x,y]', children: [], disabled: true },
|
||||
{ name: 'matchedDescription', type: 'string', desc: '模型实际定位的目标描述', disabled: true },
|
||||
{ name: 'refinementMethod', type: 'string', desc: '坐标精修方式', disabled: true },
|
||||
{ name: 'matchCount', type: 'number', desc: '匹配项数量', disabled: true },
|
||||
{ name: 'matches', type: 'array<object>', desc: '全部匹配项', children: [], disabled: true },
|
||||
{ name: 'selectedMatchIndex', type: 'number', desc: '当前选择的匹配序号', disabled: true },
|
||||
])
|
||||
|
||||
const DEVICE_KIND_META = Object.freeze({
|
||||
@ -104,6 +113,21 @@ export const normalizeFlowResourceSchema = (sourceGraph) => {
|
||||
graph.nodes.forEach((node) => {
|
||||
node.type = LEGACY_NODE_TYPE_ALIASES[node.type] || node.type
|
||||
// 早期图片分析图数据可能只保存节点类型,加载时补齐运行端识别所需的协议字段。
|
||||
const properties = node.properties || (node.properties = {})
|
||||
if (['AGV_MOVE_TO_POINT', 'AGV_MOVE_TO_STATION'].includes(properties.action)) {
|
||||
properties.nodeParams = Array.isArray(properties.nodeParams) ? properties.nodeParams : []
|
||||
if (!properties.nodeParams.some((param) => param?.name === 'blockedTimeoutMs')) {
|
||||
properties.nodeParams.push({
|
||||
name: 'blockedTimeoutMs',
|
||||
type: 'input',
|
||||
componentType: 'number',
|
||||
input: 60000,
|
||||
min: 1,
|
||||
required: true,
|
||||
disabled: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (node.type === 'imageAnalysis') {
|
||||
const properties = node.properties || (node.properties = {})
|
||||
properties.action = 'IMAGE_ANALYZE'
|
||||
|
||||
@ -8,6 +8,11 @@
|
||||
<div class="task-log__body">
|
||||
<div class="task-log__content">{{ log.logContent }}</div>
|
||||
|
||||
<div v-if="errorOf(log)" class="task-log__error">
|
||||
<strong>失败原因</strong>
|
||||
<span>{{ errorOf(log) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 只有 analysisOf 能确认是音频或视频分析日志时,才展示结构化分析结果。 -->
|
||||
<div v-if="analysisOf(log)" class="task-log__analysis">
|
||||
<!-- 标题行展示分析类型、执行状态、模型名称和耗时;缺失字段不会留下空占位。 -->
|
||||
@ -158,6 +163,28 @@ const addMedia = (result, seen, type, value) => {
|
||||
// 分析结果的计算相对复杂,使用 WeakMap 保证模板多次调用 analysisOf(log) 时只解析一次。
|
||||
const analysisCache = new WeakMap()
|
||||
|
||||
const errorCache = new WeakMap()
|
||||
const errorOf = (log) => {
|
||||
if (!log || typeof log !== 'object') return ''
|
||||
if (errorCache.has(log)) return errorCache.get(log)
|
||||
const actualResult = parseObject(log.actualResult)
|
||||
const extraInfo = parseObject(log.extraInfo)
|
||||
const status = String(log.status ?? log.logStatus ?? '').trim().toLowerCase()
|
||||
const failed = ['2', 'failed', 'failure', 'error'].includes(status)
|
||||
const candidates = [
|
||||
log.errorMessage,
|
||||
log.errorStack,
|
||||
actualResult.errorMessage,
|
||||
actualResult.error,
|
||||
extraInfo.errorMessage,
|
||||
extraInfo.error,
|
||||
failed ? log.message : '',
|
||||
]
|
||||
const result = candidates.find(value => typeof value === 'string' && value.trim())?.trim() || ''
|
||||
errorCache.set(log, result)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 把日志中的音视频分析结果转换成模板直接使用的展示模型。
|
||||
* 返回 null 表示该日志不是可展示的音视频分析结果;否则返回状态、模型、置信度、结论和扩展详情等字段。
|
||||
@ -424,6 +451,24 @@ onMounted(scrollToBottom)
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.task-log__error {
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
margin-top: 7px;
|
||||
padding: 8px 10px;
|
||||
border-left: 3px solid var(--el-color-danger);
|
||||
background: var(--el-color-danger-light-9);
|
||||
color: var(--el-color-danger-dark-2);
|
||||
font-size: 13px;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
.task-log__error span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* 分析结果使用左侧强调线与普通日志正文区分,但保持在同一条日志的内容列中。 */
|
||||
.task-log__analysis {
|
||||
margin-top: 9px;
|
||||
|
||||
@ -186,23 +186,26 @@ const fieldsByType = {
|
||||
AGV_NAVIGATE_TO_POSE: [
|
||||
...['x', 'y', 'theta'].map((key) => ({ key, label: key === 'theta' ? '角度' : key.toUpperCase(), number: true })),
|
||||
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
|
||||
{ key: 'blockedTimeoutMs', label: '遇障超时(毫秒)', number: true, min: 1, step: 1000 },
|
||||
],
|
||||
AGV_NAVIGATE_TO_STATION: [
|
||||
{ key: 'stationId', label: '站点 ID' },
|
||||
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
|
||||
{ key: 'blockedTimeoutMs', label: '遇障超时(毫秒)', number: true, min: 1, step: 1000 },
|
||||
],
|
||||
AGV_FOLLOW_PATH: [
|
||||
{ key: 'path', label: '路径段(JSON 数组)', multiline: true },
|
||||
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
|
||||
{ key: 'blockedTimeoutMs', label: '遇障超时(毫秒)', number: true, min: 1, step: 1000 },
|
||||
],
|
||||
}
|
||||
const defaultParams = {
|
||||
DELAY: { durationMs: 1000 },
|
||||
ARM_MOVE_J: { positions: '[0, 0, 0, 0, 0, 0]', velocity: 0.1, acceleration: 0.2 },
|
||||
ARM_MOVE_L: { x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0, frame: 'BASE', velocity: 2.1, acceleration: 2.2 },
|
||||
AGV_NAVIGATE_TO_POSE: { x: 0, y: 0, theta: 0, maxSpeed: 0.5 },
|
||||
AGV_NAVIGATE_TO_STATION: { stationId: '', maxSpeed: 0.5 },
|
||||
AGV_FOLLOW_PATH: { path: '[]', maxSpeed: 0.5 },
|
||||
AGV_NAVIGATE_TO_POSE: { x: 0, y: 0, theta: 0, maxSpeed: 0.5, blockedTimeoutMs: 60000 },
|
||||
AGV_NAVIGATE_TO_STATION: { stationId: '', maxSpeed: 0.5, blockedTimeoutMs: 60000 },
|
||||
AGV_FOLLOW_PATH: { path: '[]', maxSpeed: 0.5, blockedTimeoutMs: 60000 },
|
||||
}
|
||||
const defaultTimeoutByType = {
|
||||
DELAY: 5000,
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
:rules="testRule(property)"
|
||||
:prop="`nodeParams.${index}.input`">
|
||||
<el-input-number class="param-value" v-if="property.componentType === 'number'"
|
||||
v-model="property.input" :min="0" :max="property.max || Infinity"
|
||||
v-model="property.input" :min="property.min ?? 0" :max="property.max || Infinity"
|
||||
:controls="property?.controls || true" :step-strictly="true" :step="property.step || 1" placeholder="请输入" clearable />
|
||||
<el-select v-model="property.input" class="param-value"
|
||||
v-else-if="property.componentType === 'select'">
|
||||
|
||||
@ -10,7 +10,11 @@
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-position="top">
|
||||
<el-form-item label="图片来源" prop="imageMode">
|
||||
<el-form-item label="分析方式">
|
||||
<el-segmented v-model="formData.analysisMethod" :options="methodOptions" block />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="isReferenceMatch ? '大图来源' : (isTextLocate ? '目标图片来源' : '图片来源')" prop="imageMode">
|
||||
<el-segmented v-model="formData.imageMode" :options="sourceOptions" />
|
||||
</el-form-item>
|
||||
|
||||
@ -25,7 +29,7 @@
|
||||
@visible-change="loadQuoteOptions"
|
||||
@change="handleQuoteChange"
|
||||
/>
|
||||
<div class="field-hint">上游输出为图片数组时,将按原顺序全部分析,最多 12 张。</div>
|
||||
<div class="field-hint" v-if="isVisionAnalysis">上游输出为图片数组时,将按原顺序全部分析,最多 12 张。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-else label="图片地址" prop="imageUrlsText">
|
||||
@ -33,27 +37,73 @@
|
||||
v-model="formData.imageUrlsText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="每行一个 HTTP/HTTPS 图片地址,最多 12 张"
|
||||
:placeholder="isLocationMethod ? '输入一张目标图片的 HTTP/HTTPS 地址' : '每行一个 HTTP/HTTPS 图片地址,最多 12 张'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分析提示词" prop="prompt">
|
||||
<el-form-item v-if="!isReferenceMatch" :label="isTextLocate ? '目标描述' : '分析提示词'" prop="prompt">
|
||||
<el-input
|
||||
v-model="formData.prompt"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
maxlength="4000"
|
||||
show-word-limit
|
||||
placeholder="例如:读取图片中的仪表盘数值,只输出数字。或:识别所有仪表,按 JSON 输出名称、数值和单位。"
|
||||
:placeholder="isTextLocate ? '例如:底部应用栏中的设置按钮' : '例如:读取图片中的仪表盘数值,只输出数字。或:识别所有仪表,按 JSON 输出名称、数值和单位。'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分析模式">
|
||||
<el-form-item v-if="!isReferenceMatch" 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-switch v-model="formData.asynchronous" />
|
||||
<div class="field-hint">开启后分析在后台并行执行,流程结束前统一等待分析结果。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isTextLocate" label="最低定位置信度">
|
||||
<el-slider v-model="formData.analysisTuning.locateConfidenceThreshold" :min="0.2" :max="0.95" :step="0.01" show-input />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="isReferenceMatch">
|
||||
<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%"
|
||||
@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>
|
||||
<el-form-item label="最低匹配分数">
|
||||
<el-slider v-model="formData.analysisTuning.matchThreshold" :min="0.2" :max="0.95" :step="0.01" show-input />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择匹配结果">
|
||||
<el-select v-model="formData.analysisTuning.matchSelector" style="width: 100%">
|
||||
<el-option v-for="item in matchSelectorOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="formData.analysisTuning.matchSelector === 'INDEX'">
|
||||
<el-form-item label="排序方式">
|
||||
<el-segmented v-model="formData.analysisTuning.matchSort" :options="matchSortOptions" block />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择第几个">
|
||||
<el-input-number v-model="formData.analysisTuning.matchIndex" :min="1" :max="20" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<el-collapse v-if="!isReferenceMatch" v-model="advancedSections" class="advanced-settings">
|
||||
<el-collapse-item v-if="isVisionAnalysis" title="参考图片(可选)" name="reference">
|
||||
<el-form-item label="参考图片来源">
|
||||
<el-segmented v-model="formData.referenceMode" :options="sourceOptions" />
|
||||
</el-form-item>
|
||||
@ -80,7 +130,7 @@
|
||||
<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-form-item v-if="isVisionAnalysis" label="最多分析图片">
|
||||
<el-input-number v-model="formData.analysisTuning.maxImages" :min="1" :max="12" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大输出长度">
|
||||
@ -100,7 +150,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { getInput } from '@/utils/flow'
|
||||
import { IMAGE_ANALYSIS_OUTPUT_PARAMS } from '@/utils/flowResources'
|
||||
|
||||
@ -122,11 +172,29 @@ const modeOptions = [
|
||||
{ label: '快速', value: 'FAST' },
|
||||
{ label: '精准', value: 'ACCURATE' },
|
||||
]
|
||||
const methodOptions = [
|
||||
{ label: '智能分析', value: 'VISION_MODEL' },
|
||||
{ label: '描述找图', value: 'TEXT_LOCATE' },
|
||||
{ label: '精确找图', value: 'REFERENCE_MATCH' },
|
||||
]
|
||||
const matchSelectorOptions = [
|
||||
{ label: '最佳匹配', value: 'BEST' },
|
||||
{ label: '按序号选择', value: 'INDEX' },
|
||||
{ label: '左上', value: 'TOP_LEFT' },
|
||||
{ label: '右上', value: 'TOP_RIGHT' },
|
||||
{ label: '左下', value: 'BOTTOM_LEFT' },
|
||||
{ label: '右下', value: 'BOTTOM_RIGHT' },
|
||||
]
|
||||
const matchSortOptions = [
|
||||
{ label: '先行后列', value: 'ROW_MAJOR' },
|
||||
{ label: '先列后行', value: 'COLUMN_MAJOR' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 使用稳定的 reactive 对象承载表单状态;initData 只覆盖字段,避免 Element Plus 校验引用失效。
|
||||
*/
|
||||
const formData = reactive({
|
||||
analysisMethod: 'VISION_MODEL',
|
||||
imageMode: 'quote',
|
||||
imageQuote: [],
|
||||
imageQuoteType: 'output',
|
||||
@ -137,12 +205,36 @@ const formData = reactive({
|
||||
referenceImageUrl: '',
|
||||
prompt: '',
|
||||
analysisMode: 'FAST',
|
||||
analysisTuning: { maxWidth: 896, maxImages: 12, maxOutputTokens: 384 },
|
||||
asynchronous: false,
|
||||
analysisTuning: {
|
||||
maxWidth: 896,
|
||||
maxImages: 12,
|
||||
maxOutputTokens: 384,
|
||||
matchThreshold: 0.48,
|
||||
matchSelector: 'BEST',
|
||||
matchIndex: 1,
|
||||
matchSort: 'ROW_MAJOR',
|
||||
locateConfidenceThreshold: 0.45,
|
||||
},
|
||||
})
|
||||
|
||||
const isReferenceMatch = computed(() => formData.analysisMethod === 'REFERENCE_MATCH')
|
||||
const isTextLocate = computed(() => formData.analysisMethod === 'TEXT_LOCATE')
|
||||
const isVisionAnalysis = computed(() => formData.analysisMethod === 'VISION_MODEL')
|
||||
const isLocationMethod = computed(() => isReferenceMatch.value || isTextLocate.value)
|
||||
|
||||
const outputSummary = [
|
||||
{ name: 'result', desc: '按提示词返回的结果;JSON、数组和数字会自动解析' },
|
||||
{ name: 'resultText', desc: '模型返回的原始文本' },
|
||||
{ name: 'matched', desc: '找图结果是否达到最低分数' },
|
||||
{ name: 'matchScore', desc: '找图的匹配分数或定位置信度' },
|
||||
{ name: 'coordinates', desc: '大图原始尺寸坐标 [x1,y1,x2,y2]' },
|
||||
{ name: 'center', desc: '目标中心点坐标 [x,y]' },
|
||||
{ name: 'matchedDescription', desc: '模型实际定位的目标描述' },
|
||||
{ name: 'refinementMethod', desc: '坐标精修方式' },
|
||||
{ name: 'matchCount', desc: '找到的匹配项数量' },
|
||||
{ name: 'matches', desc: '全部匹配项及其序号、坐标和分数' },
|
||||
{ name: 'selectedMatchIndex', desc: '当前选择的匹配序号' },
|
||||
]
|
||||
|
||||
// 文本输入允许换行或逗号分隔,保存前统一转成执行端需要的 URL 数组。
|
||||
@ -167,7 +259,8 @@ const rules = {
|
||||
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 张图片'))
|
||||
const maximum = isLocationMethod.value ? 1 : 12
|
||||
if (urls.length > maximum) return Promise.reject(new Error(isLocationMethod.value ? '找图只能输入一张目标图片' : '最多输入 12 张图片'))
|
||||
return urls.every(validUrl)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('图片地址必须是 HTTP/HTTPS 地址'))
|
||||
@ -175,12 +268,27 @@ const rules = {
|
||||
trigger: 'blur',
|
||||
}],
|
||||
referenceImageUrl: [{
|
||||
validator: (_, value) => !value || formData.referenceMode !== 'input' || validUrl(String(value).trim())
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请输入有效的参考图片地址')),
|
||||
validator: (_, value) => {
|
||||
if (formData.referenceMode !== 'input') return Promise.resolve()
|
||||
if (isReferenceMatch.value && !String(value || '').trim()) return Promise.reject(new Error('请输入待查小图地址'))
|
||||
return !value || validUrl(String(value).trim())
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请输入有效的参考图片地址'))
|
||||
},
|
||||
trigger: 'blur',
|
||||
}],
|
||||
referenceQuote: [{
|
||||
validator: (_, value) => !isReferenceMatch.value || formData.referenceMode !== 'quote' || value?.length
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请选择待查小图输出')),
|
||||
trigger: 'change',
|
||||
}],
|
||||
prompt: [{
|
||||
validator: (_, value) => isReferenceMatch.value || String(value || '').trim()
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请输入分析提示词')),
|
||||
trigger: 'blur',
|
||||
}],
|
||||
prompt: [{ required: true, message: '请输入分析提示词', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
// 引用选项按需加载,上游流程较大时避免每次渲染都遍历画布。
|
||||
@ -216,22 +324,41 @@ const initData = () => {
|
||||
const images = getParam(params, 'imageUrl')
|
||||
const reference = getParam(params, 'referenceImageUrl')
|
||||
const tuning = getParam(params, 'analysisTuning')?.input || {}
|
||||
const asynchronous = getParam(params, 'asynchronous')
|
||||
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 || '')
|
||||
const staticImages = Array.isArray(images?.input) ? [...images.input] : parseUrls(images?.input || '')
|
||||
const savedMethod = String(getParam(params, 'analysisMethod')?.input || '').toUpperCase()
|
||||
const savedPrompt = getParam(params, 'prompt')?.input || legacyPrompt(params)
|
||||
const coordinatePrompt = /(坐标|位置|小图|参考图|图标|coordinate|position|bbox|template)/i.test(savedPrompt)
|
||||
const legacyReferenceMatch = !savedMethod && coordinatePrompt
|
||||
&& ((reference && staticImages.length === 1) || (!reference && staticImages.length === 2))
|
||||
const legacyTextLocate = !savedMethod && coordinatePrompt && !reference && staticImages.length === 1
|
||||
formData.analysisMethod = savedMethod === 'REFERENCE_MATCH' || legacyReferenceMatch
|
||||
? 'REFERENCE_MATCH'
|
||||
: (savedMethod === 'TEXT_LOCATE' || legacyTextLocate ? 'TEXT_LOCATE' : 'VISION_MODEL')
|
||||
formData.imageUrlsText = (legacyReferenceMatch ? staticImages.slice(0, 1) : staticImages).join('\n')
|
||||
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)
|
||||
formData.referenceImageUrl = reference?.input || (legacyReferenceMatch ? staticImages[1] : '')
|
||||
formData.prompt = savedPrompt
|
||||
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.maxImages = Number(tuning.maxImages) || 12
|
||||
formData.analysisTuning.maxOutputTokens = Number(tuning.maxOutputTokens)
|
||||
|| (formData.analysisMode === 'FAST' ? 384 : 512)
|
||||
advancedSections.value = hasReference() ? ['reference'] : []
|
||||
formData.analysisTuning.matchThreshold = Number(tuning.matchThreshold) || 0.48
|
||||
formData.analysisTuning.matchSelector = matchSelectorOptions.some(item => item.value === tuning.matchSelector)
|
||||
? tuning.matchSelector
|
||||
: 'BEST'
|
||||
formData.analysisTuning.matchIndex = Math.max(1, Math.min(20, Number(tuning.matchIndex) || 1))
|
||||
formData.analysisTuning.matchSort = tuning.matchSort === 'COLUMN_MAJOR' ? 'COLUMN_MAJOR' : 'ROW_MAJOR'
|
||||
formData.analysisTuning.locateConfidenceThreshold = Number(tuning.locateConfidenceThreshold) || 0.45
|
||||
advancedSections.value = isVisionAnalysis.value && hasReference() ? ['reference'] : []
|
||||
}
|
||||
watch(() => props.data, initData, { immediate: true, deep: true })
|
||||
|
||||
@ -257,13 +384,15 @@ const validateAndSave = async () => {
|
||||
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: 'analysisMethod', type: 'input', input: formData.analysisMethod, required: false, disabled: true },
|
||||
{ name: 'prompt', type: 'input', input: isReferenceMatch.value ? '在大图中查找小图,返回原图像素坐标 [x1,y1,x2,y2]' : 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 },
|
||||
{ name: 'asynchronous', type: 'input', input: formData.asynchronous, required: false, disabled: true, componentType: 'switch' },
|
||||
]
|
||||
if (hasReference()) {
|
||||
if (!isTextLocate.value && hasReference()) {
|
||||
// 参考图存在时插入到图片来源和提示词之间,保持执行端按固定顺序读取。
|
||||
nodeParams.splice(2, 0, buildMediaParam(
|
||||
nodeParams.splice(3, 0, buildMediaParam(
|
||||
'referenceImageUrl', formData.referenceMode, formData.referenceImageUrl.trim(),
|
||||
formData.referenceQuote, formData.referenceQuoteType, false,
|
||||
))
|
||||
|
||||
@ -81,6 +81,11 @@
|
||||
<el-input v-model="formData.mediaUrl" type="textarea" :rows="3" placeholder="请输入 HTTP/HTTPS 媒体地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="异步执行">
|
||||
<el-switch v-model="formData.asynchronous" />
|
||||
<div class="field-hint">开启后分析在后台并行执行,流程结束前统一等待分析结果。</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 视频模型依赖自然语言标准判断是否通过;音频节点使用 expectedLabel,不需要 instruction。 -->
|
||||
<el-form-item v-if="isVideo" label="通过标准" prop="instruction" required>
|
||||
<el-input
|
||||
@ -201,6 +206,7 @@ const formData = reactive({
|
||||
instruction: '',
|
||||
analysisMode: 'AUTO',
|
||||
detailedOutput: false,
|
||||
asynchronous: false,
|
||||
analysisTuning: { ...analysisPresets.AUTO },
|
||||
})
|
||||
|
||||
@ -395,6 +401,7 @@ const initData = () => {
|
||||
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 asynchronous = params.find(item => item.name === 'asynchronous')
|
||||
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
||||
formData.expectedLabel = expectedLabel?.input || ''
|
||||
formData.expectedLabelName = expectedLabelName?.input || ''
|
||||
@ -409,6 +416,7 @@ const initData = () => {
|
||||
formData.analysisMode = analysisPresets[analysisMode?.input] ? analysisMode.input : 'AUTO'
|
||||
// 兼容旧流程或外部写入的字符串值,表单内部统一使用布尔值驱动开关。
|
||||
formData.detailedOutput = detailedOutput?.input === true || detailedOutput?.input === 'true'
|
||||
formData.asynchronous = asynchronous?.input === true || asynchronous?.input === 'true'
|
||||
const preset = analysisPresets[formData.analysisMode] || analysisPresets.AUTO
|
||||
Object.assign(formData.analysisTuning, normalizeStoredTuning(analysisTuning?.input, preset))
|
||||
if (formData.analysisMode !== 'AUTO') formData.analysisTuning.fallbackToAccurate = false
|
||||
@ -452,6 +460,14 @@ const validateAndSave = async () => {
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
nodeParams.push({
|
||||
name: 'asynchronous',
|
||||
type: 'input',
|
||||
input: formData.asynchronous,
|
||||
required: false,
|
||||
disabled: true,
|
||||
componentType: 'switch',
|
||||
})
|
||||
// 音频节点把目标类型插入 profileCode 与媒体参数之间,不保存任何视频专属参数。
|
||||
if (!isVideo.value) {
|
||||
nodeParams.splice(1, 0,
|
||||
|
||||
@ -307,6 +307,7 @@ function handler(params) {
|
||||
{ name: "x", type: "input", input: "", disabled: true },
|
||||
{ name: "y", type: "input", input: "", disabled: true },
|
||||
{ name: "theta", type: "input", input: "", disabled: true },
|
||||
{ name: "blockedTimeoutMs", type: "input", componentType: "number", input: 60000, min: 1, required: true, disabled: true },
|
||||
],
|
||||
outputType: 'json'
|
||||
},
|
||||
@ -318,7 +319,8 @@ function handler(params) {
|
||||
action: 'AGV_MOVE_TO_STATION',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "stationId", type: "input", input: "", componentType: 'select', selectOptions: [], forceSelect: true, disabled: true }
|
||||
{ name: "stationId", type: "input", input: "", componentType: 'select', selectOptions: [], forceSelect: true, disabled: true },
|
||||
{ name: "blockedTimeoutMs", type: "input", componentType: "number", input: 60000, min: 1, required: true, disabled: true }
|
||||
],
|
||||
outputType: 'json'
|
||||
},
|
||||
@ -530,6 +532,7 @@ function handler(params) {
|
||||
{ name: "expectedLabel", type: "input", input: "", required: true, disabled: true },
|
||||
{ name: "expectedLabelName", type: "input", input: "", required: false, disabled: true },
|
||||
{ name: "audioUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
|
||||
{ name: "asynchronous", type: "input", input: false, required: false, disabled: true, componentType: "switch" },
|
||||
],
|
||||
outputType: "json",
|
||||
outputParams: [
|
||||
@ -559,6 +562,7 @@ function handler(params) {
|
||||
// 默认返回精简结论;用户开启详细输出时,执行端才补充事件证据。
|
||||
{ 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: "asynchronous", type: "input", input: false, required: false, disabled: true, componentType: "switch" },
|
||||
],
|
||||
outputType: "json",
|
||||
outputParams: [
|
||||
@ -586,14 +590,25 @@ function handler(params) {
|
||||
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: "analysisMethod", type: "input", input: "VISION_MODEL", required: false, 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 },
|
||||
{ name: "analysisTuning", type: "input", input: { maxWidth: 896, maxImages: 12, maxOutputTokens: 384, matchThreshold: 0.48, matchSelector: "BEST", matchIndex: 1, matchSort: "ROW_MAJOR", locateConfidenceThreshold: 0.45 }, required: false, disabled: true },
|
||||
{ name: "asynchronous", type: "input", input: false, required: false, disabled: true, componentType: "switch" },
|
||||
],
|
||||
outputType: "json",
|
||||
outputParams: [
|
||||
{ name: "result", type: "object", desc: "按提示词返回的结果", children: [], disabled: true },
|
||||
{ name: "resultText", type: "string", desc: "模型返回的原始文本", disabled: true },
|
||||
{ name: "matched", type: "boolean", desc: "找图是否匹配", disabled: true },
|
||||
{ name: "matchScore", type: "number", desc: "匹配分数或定位置信度", disabled: true },
|
||||
{ name: "coordinates", type: "array<number>", desc: "原图坐标 [x1,y1,x2,y2]", children: [], disabled: true },
|
||||
{ name: "center", type: "array<number>", desc: "目标中心点 [x,y]", children: [], disabled: true },
|
||||
{ name: "matchedDescription", type: "string", desc: "模型实际定位的目标描述", disabled: true },
|
||||
{ name: "refinementMethod", type: "string", desc: "坐标精修方式", disabled: true },
|
||||
{ name: "matchCount", type: "number", desc: "匹配项数量", disabled: true },
|
||||
{ name: "matches", type: "array<object>", desc: "全部匹配项", children: [], disabled: true },
|
||||
{ name: "selectedMatchIndex", type: "number", desc: "当前选择的匹配序号", disabled: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user