From 71feae4bbd9579a100a9c1d63e37b60f08bf7d5a Mon Sep 17 00:00:00 2001
From: lixiaolong <702156524@qq.com>
Date: Wed, 19 Aug 2026 15:00:17 +0800
Subject: [PATCH] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0=E9=80=9A?=
=?UTF-8?q?=E7=94=A8=E5=9B=BE=E7=89=87=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E5=AA=92=E4=BD=93=E5=88=86=E6=9E=90?=
=?UTF-8?q?=E8=8A=82=E7=82=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增通用图片分析节点类型,支持按提示词分析图片并返回指定内容
- 添加详细的图片分析参数配置界面,包括图片源选择、提示词输入等功能
- 实现图片分析节点的输出参数定义和处理逻辑
- 更新视频分析节点,增加详细输出选项和结果字段
- 重构错误状态恢复功能,更名为运行状态恢复并更新相关UI
- 添加图片分析结果的日志展示支持
- 优化流程资源配置,增加图片分析相关的常量定义
- 更新单节点执行参数构建逻辑,适配新的图片分析功能
---
src/api/device/system.js | 11 +
src/utils/flowResources.js | 25 ++
src/views/flow/components/ParamsDrawer.vue | 2 +
.../flow/components/TaskExecutionLog.vue | 20 +-
.../components/params/ImageAnalysisParams.vue | 274 ++++++++++++++++++
.../components/params/MediaAnalysisParams.vue | 23 ++
src/views/flow/config.js | 23 ++
src/views/flow/index.vue | 133 ++++++---
.../intelligenceTest/runningTask/task-log.vue | 2 +
9 files changed, 467 insertions(+), 46 deletions(-)
create mode 100644 src/views/flow/components/params/ImageAnalysisParams.vue
diff --git a/src/api/device/system.js b/src/api/device/system.js
index 18508dc..55356a9 100644
--- a/src/api/device/system.js
+++ b/src/api/device/system.js
@@ -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) {
return request({
url: '/api/system/recoverSafetyState',
diff --git a/src/utils/flowResources.js b/src/utils/flowResources.js
index a761b30..b6f8a98 100644
--- a/src/utils/flowResources.js
+++ b/src/utils/flowResources.js
@@ -4,6 +4,10 @@ export const FLOW_SCHEMA_VERSION = 2
export const DEFAULT_ROLE_KEY = 'default_executor'
export const FLOW_NODE_RESOURCE_OWNER = 'FLOW_NODE'
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({
1: { key: 'agv', name: '移动底盘' },
@@ -98,6 +102,27 @@ export const normalizeFlowResourceSchema = (sourceGraph) => {
graph.edges = Array.isArray(graph.edges) ? graph.edges : []
graph.nodes.forEach((node) => {
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)
if (!start) return graph
diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue
index 6b307bf..c33a11f 100644
--- a/src/views/flow/components/ParamsDrawer.vue
+++ b/src/views/flow/components/ParamsDrawer.vue
@@ -40,6 +40,7 @@ import DeviceUniversal from './params/DeviceUniversal.vue'
import SwitchNodeParams from './params/SwitchNodeParams.vue'
import ActionQueueParams from './params/ActionQueueParams.vue'
import MediaAnalysisParams from './params/MediaAnalysisParams.vue'
+import ImageAnalysisParams from './params/ImageAnalysisParams.vue'
const props = defineProps({
drawer: Boolean,
@@ -68,6 +69,7 @@ const currentComponent = computed(() => {
if (['deviceUniversal', 'device_universal'].includes(type)) return DeviceUniversal
if (type === 'actionQueue') return ActionQueueParams
if (type === 'mediaAnalysis') return MediaAnalysisParams
+ if (type === 'imageAnalysis') return ImageAnalysisParams
if (type === 'branch') return SwitchNodeParams
return null
})
diff --git a/src/views/flow/components/TaskExecutionLog.vue b/src/views/flow/components/TaskExecutionLog.vue
index 00e1846..c088627 100644
--- a/src/views/flow/components/TaskExecutionLog.vue
+++ b/src/views/flow/components/TaskExecutionLog.vue
@@ -178,6 +178,7 @@ const analysisOf = (log) => {
const profileCode = String(output.profileCode || '').toLowerCase()
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 isImage = analysisType.includes('IMAGE') || profileCode.includes('image')
// 仅有媒体类型还不足以展示分析面板,至少要存在一个分析状态或业务结果字段。
const hasResult = Boolean(
@@ -188,11 +189,13 @@ const analysisOf = (log) => {
|| output.labelName
|| output.summary
|| output.conclusion
+ || output.resultText
+ || output.result !== undefined
|| output.passed !== undefined
)
// 非分析日志也缓存 null,避免之后每次渲染都重复执行同一套识别逻辑。
- if (!hasResult || (!isAudio && !isVideo)) {
+ if (!hasResult || (!isAudio && !isVideo && !isImage)) {
analysisCache.set(log, null)
return null
}
@@ -223,9 +226,9 @@ const analysisOf = (log) => {
// 它与已经结构化展示的分析字段一起从“详情”中移除,避免用户看到重复且难以理解的底层上下文。
;[
'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',
- 'expectedLabel', 'expectedLabelName', 'decisionReason',
+ 'expectedLabel', 'expectedLabelName', 'decisionReason', 'resultText', 'result',
].forEach((key) => delete detailSource[key])
// 所有已知字段删除后还有剩余内容时,才生成详情 JSON,避免显示空对象。
@@ -233,7 +236,7 @@ const analysisOf = (log) => {
// 统一生成展示模型,模板不需要了解各版本日志字段和状态枚举的差异。
const result = {
- title: isAudio ? '声音类型检测' : '视频分析',
+ title: isAudio ? '声音类型检测' : isImage ? '通用图片分析' : '视频分析',
statusLabel,
// 分析失败或业务判断未通过使用红色;仅匹配失败使用警告色,其余使用成功色。
@@ -249,7 +252,10 @@ const analysisOf = (log) => {
matchLabel: isAudio ? '' : matched === true ? '匹配' : matched === false ? '未匹配' : '',
confidence,
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,
events: Array.isArray(output.events)
? output.events.slice(0, 20).map((event) => typeof event === 'string' ? event : JSON.stringify(event))
@@ -289,6 +295,10 @@ const mediaOf = (log) => {
// mediaUrl 本身没有类型,必须结合分析类型或 profileCode 后才能作为视频/音频加入。
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('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, 'video', log.videoUrl)
diff --git a/src/views/flow/components/params/ImageAnalysisParams.vue b/src/views/flow/components/params/ImageAnalysisParams.vue
new file mode 100644
index 0000000..9c1d4df
--- /dev/null
+++ b/src/views/flow/components/params/ImageAnalysisParams.vue
@@ -0,0 +1,274 @@
+
+ {{ item.name }}{{ item.desc }}
+