Restore AI workflow design nodes
This commit is contained in:
@@ -0,0 +1,483 @@
|
||||
"""
|
||||
仪表盘设计节点
|
||||
|
||||
提供可视化设计界面,让用户设计仪表盘布局
|
||||
"""
|
||||
import ast
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from ..base import BaseNode, NodeContext, NodeResult
|
||||
from ..registry import NodeRegistry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@NodeRegistry.register
|
||||
class DashboardDesignNode(BaseNode):
|
||||
"""
|
||||
仪表盘设计节点
|
||||
|
||||
提供可视化设计界面,让用户设计仪表盘布局和组件
|
||||
"""
|
||||
|
||||
node_type = 'dashboard_design'
|
||||
node_name = '仪表盘设计'
|
||||
node_category = 'dashboard'
|
||||
node_icon = 'layout-dashboard'
|
||||
node_description = '可视化设计仪表盘布局和组件'
|
||||
|
||||
inputs = [
|
||||
{
|
||||
'name': 'dashboard_code',
|
||||
'type': 'string',
|
||||
'description': '仪表盘编码(来自基础信息节点)',
|
||||
'required': False,
|
||||
},
|
||||
{
|
||||
'name': 'design_suggestion',
|
||||
'type': 'string',
|
||||
'description': 'LLM生成的设计建议(可选)',
|
||||
'required': False,
|
||||
},
|
||||
{
|
||||
'name': 'initial_config',
|
||||
'type': 'object',
|
||||
'description': '初始页面配置(可选)',
|
||||
'required': False,
|
||||
},
|
||||
]
|
||||
|
||||
outputs = [
|
||||
{
|
||||
'name': 'page_config',
|
||||
'type': 'object',
|
||||
'description': '页面设计配置(dashboard-design的JSON配置)',
|
||||
},
|
||||
{
|
||||
'name': 'design_title',
|
||||
'type': 'string',
|
||||
'description': '设计方案标题',
|
||||
},
|
||||
{
|
||||
'name': 'confirmed',
|
||||
'type': 'boolean',
|
||||
'description': '是否已确认',
|
||||
},
|
||||
]
|
||||
|
||||
def execute(self, context: NodeContext) -> NodeResult:
|
||||
"""执行节点"""
|
||||
try:
|
||||
# 检查是否有用户提交的设计配置
|
||||
user_input = context.variables.get('__user_input__')
|
||||
if user_input:
|
||||
design_data = self._parse_user_design(user_input)
|
||||
if design_data:
|
||||
logger.info(f'DashboardDesignNode - 使用用户设计的配置')
|
||||
context.variables.pop('__user_input__', None)
|
||||
return self._build_result(design_data, confirmed=True)
|
||||
|
||||
# 获取输入参数
|
||||
dashboard_code = context.resolve_template(self.config.get('dashboard_code', ''))
|
||||
design_suggestion = context.resolve_template(self.config.get('design_suggestion', ''))
|
||||
initial_config = self.config.get('initial_config', {})
|
||||
|
||||
# 如果initial_config是变量引用,解析它
|
||||
if isinstance(initial_config, str) and initial_config.startswith('{{'):
|
||||
initial_config = self._resolve_variable_value(context, initial_config) or {}
|
||||
|
||||
logger.info(f'DashboardDesignNode - dashboard_code={dashboard_code}')
|
||||
|
||||
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
|
||||
require_confirmation = self.resolve_require_confirmation(context, default=True)
|
||||
design_title = self.config.get('design_title', '仪表盘设计')
|
||||
|
||||
# 如果没有初始配置,根据 design_suggestion 生成默认模板
|
||||
if not initial_config or not initial_config.get('widgets'):
|
||||
initial_config = self._generate_dashboard_template(design_suggestion, dashboard_code)
|
||||
|
||||
# 构建设计数据
|
||||
design_data = {
|
||||
'dashboard_code': dashboard_code,
|
||||
'design_title': design_title,
|
||||
'design_suggestion': design_suggestion,
|
||||
'page_config': initial_config,
|
||||
}
|
||||
|
||||
if require_confirmation:
|
||||
return NodeResult(
|
||||
success=True,
|
||||
output={
|
||||
'page_config': design_data.get('page_config', {}),
|
||||
'design_title': design_title,
|
||||
'confirmed': False,
|
||||
},
|
||||
waiting_for_input=True,
|
||||
waiting_config={
|
||||
'type': 'design_preview',
|
||||
'preview_type': 'dashboard_design',
|
||||
'title': design_title,
|
||||
'data': design_data,
|
||||
'editable': True,
|
||||
'fullscreen': True,
|
||||
},
|
||||
)
|
||||
|
||||
return self._build_result(design_data, confirmed=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'DashboardDesignNode - 执行失败: {str(e)}')
|
||||
return NodeResult(
|
||||
success=False,
|
||||
error=f'仪表盘设计节点执行失败: {str(e)}',
|
||||
)
|
||||
|
||||
def _parse_user_design(self, user_input: Any) -> Optional[Dict[str, Any]]:
|
||||
"""解析用户提交的设计配置"""
|
||||
try:
|
||||
if isinstance(user_input, str):
|
||||
data = json.loads(user_input)
|
||||
else:
|
||||
data = user_input
|
||||
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f'解析用户设计配置失败: {e}')
|
||||
return None
|
||||
|
||||
def _build_result(self, design_data: Dict[str, Any], confirmed: bool) -> NodeResult:
|
||||
"""构建节点结果"""
|
||||
page_config = design_data.get('page_config', {})
|
||||
design_title = design_data.get('design_title', '仪表盘设计')
|
||||
|
||||
output_data = {
|
||||
'page_config': page_config,
|
||||
'design_title': design_title,
|
||||
'confirmed': confirmed,
|
||||
}
|
||||
|
||||
return NodeResult(
|
||||
success=True,
|
||||
output=output_data,
|
||||
output_variables=output_data, # 添加 output_variables
|
||||
)
|
||||
|
||||
def _resolve_variable_value(self, context: NodeContext, value: Any) -> Any:
|
||||
"""解析变量引用"""
|
||||
if isinstance(value, str):
|
||||
resolved = context.resolve_template(value)
|
||||
if resolved != value:
|
||||
try:
|
||||
return json.loads(resolved) if isinstance(resolved, str) else resolved
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return resolved
|
||||
return value
|
||||
|
||||
def _generate_dashboard_template(self, design_suggestion: str, dashboard_code: str) -> Dict[str, Any]:
|
||||
"""根据 LLM 建议生成仪表盘模板"""
|
||||
try:
|
||||
# 解析 design_suggestion(可能是字符串形式的字典)
|
||||
modules = self._parse_design_suggestion(design_suggestion)
|
||||
|
||||
# 生成基础模板
|
||||
template = {
|
||||
"id": str(uuid4()),
|
||||
"name": "我的仪表盘",
|
||||
"columns": 12,
|
||||
"rowHeight": 50,
|
||||
"margin": [12, 12],
|
||||
"widgets": []
|
||||
}
|
||||
|
||||
# 添加欢迎卡片
|
||||
welcome_id = str(uuid4())
|
||||
template["widgets"].append({
|
||||
"id": welcome_id,
|
||||
"i": welcome_id,
|
||||
"type": "welcome-card",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 8,
|
||||
"h": 2,
|
||||
"minW": 4,
|
||||
"minH": 2,
|
||||
"title": "欢迎卡片",
|
||||
"props": {
|
||||
"title": "欢迎回来",
|
||||
"subtitle": "今天是个好日子",
|
||||
"showTime": True,
|
||||
"showWeather": False
|
||||
}
|
||||
})
|
||||
|
||||
# 添加天气组件
|
||||
weather_id = str(uuid4())
|
||||
template["widgets"].append({
|
||||
"id": weather_id,
|
||||
"i": weather_id,
|
||||
"type": "weather",
|
||||
"x": 8,
|
||||
"y": 0,
|
||||
"w": 4,
|
||||
"h": 2,
|
||||
"minW": 2,
|
||||
"minH": 2,
|
||||
"title": "天气",
|
||||
"props": {
|
||||
"title": "今日天气",
|
||||
"city": "北京",
|
||||
"temperature": 25,
|
||||
"weather": "晴",
|
||||
"humidity": 45,
|
||||
"wind": "东北风 3级",
|
||||
"icon": "sunny"
|
||||
}
|
||||
})
|
||||
|
||||
# 添加日历
|
||||
calendar_id = str(uuid4())
|
||||
template["widgets"].append({
|
||||
"id": calendar_id,
|
||||
"i": calendar_id,
|
||||
"type": "calendar",
|
||||
"x": 8,
|
||||
"y": 2,
|
||||
"w": 4,
|
||||
"h": 5,
|
||||
"minW": 3,
|
||||
"minH": 4,
|
||||
"title": "日历",
|
||||
"props": {
|
||||
"title": "日历",
|
||||
"showLunar": False
|
||||
}
|
||||
})
|
||||
|
||||
# 根据模块生成图表
|
||||
y_offset = 2
|
||||
if modules and len(modules) > 0:
|
||||
# 生成折线图(基于第一个模块)
|
||||
module1 = modules[0] if len(modules) > 0 else {"module_name": "数据", "moduld_fields": []}
|
||||
line_chart = self._generate_line_chart(module1, 0, y_offset)
|
||||
template["widgets"].append(line_chart)
|
||||
|
||||
y_offset += 5
|
||||
|
||||
# 生成柱状图(基于第二个模块)
|
||||
module2 = modules[1] if len(modules) > 1 else module1
|
||||
bar_chart = self._generate_bar_chart(module2, 0, y_offset)
|
||||
template["widgets"].append(bar_chart)
|
||||
|
||||
# 生成饼图(基于第三个模块)
|
||||
module3 = modules[2] if len(modules) > 2 else module1
|
||||
pie_chart = self._generate_pie_chart(module3, 8, y_offset)
|
||||
template["widgets"].append(pie_chart)
|
||||
else:
|
||||
# 没有模块信息,使用默认图表
|
||||
template["widgets"].extend([
|
||||
self._generate_line_chart({"module_name": "访问趋势", "moduld_fields": []}, 0, y_offset),
|
||||
self._generate_bar_chart({"module_name": "销售统计", "moduld_fields": []}, 0, y_offset + 5),
|
||||
self._generate_pie_chart({"module_name": "流量来源", "moduld_fields": []}, 8, y_offset + 5)
|
||||
])
|
||||
|
||||
return template
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'生成仪表盘模板失败: {e}')
|
||||
# 返回空模板
|
||||
return {
|
||||
"id": str(uuid4()),
|
||||
"name": "我的仪表盘",
|
||||
"columns": 12,
|
||||
"rowHeight": 50,
|
||||
"margin": [12, 12],
|
||||
"widgets": []
|
||||
}
|
||||
|
||||
def _parse_design_suggestion(self, design_suggestion: str) -> List[Dict[str, Any]]:
|
||||
"""解析 LLM 生成的设计建议"""
|
||||
try:
|
||||
if not design_suggestion:
|
||||
return []
|
||||
|
||||
# 尝试解析为 JSON
|
||||
if isinstance(design_suggestion, str):
|
||||
try:
|
||||
data = json.loads(design_suggestion)
|
||||
except json.JSONDecodeError:
|
||||
# 尝试使用 ast.literal_eval
|
||||
try:
|
||||
data = ast.literal_eval(design_suggestion)
|
||||
except (ValueError, SyntaxError):
|
||||
return []
|
||||
else:
|
||||
data = design_suggestion
|
||||
|
||||
# 提取模块信息
|
||||
if isinstance(data, dict):
|
||||
modules = data.get('app_modules', [])
|
||||
if isinstance(modules, list):
|
||||
return modules
|
||||
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f'解析设计建议失败: {e}')
|
||||
return []
|
||||
|
||||
def _generate_line_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]:
|
||||
"""生成折线图"""
|
||||
module_name = module.get('module_name', '访问趋势')
|
||||
fields = module.get('moduld_fields', [])
|
||||
|
||||
# 生成模拟数据
|
||||
months = ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"]
|
||||
series_data = []
|
||||
|
||||
# 根据字段生成系列(最多2个)
|
||||
for i, field in enumerate(fields[:2]):
|
||||
data = [random.randint(500, 1500) for _ in range(12)]
|
||||
series_data.append({
|
||||
"name": field if isinstance(field, str) else f"系列{i+1}",
|
||||
"data": data
|
||||
})
|
||||
|
||||
# 如果没有字段,使用默认系列
|
||||
if not series_data:
|
||||
series_data = [
|
||||
{"name": "访问量", "data": [820, 932, 901, 934, 1290, 1330, 1320, 1450, 1200, 1100, 1350, 1500]},
|
||||
{"name": "下载量", "data": [620, 732, 701, 734, 1090, 1130, 1120, 1250, 1000, 900, 1150, 1300]}
|
||||
]
|
||||
|
||||
line_id = str(uuid4())
|
||||
return {
|
||||
"id": line_id,
|
||||
"i": line_id,
|
||||
"type": "chart-line",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"w": 8,
|
||||
"h": 5,
|
||||
"minW": 4,
|
||||
"minH": 3,
|
||||
"title": "折线图",
|
||||
"props": {
|
||||
"title": module_name,
|
||||
"smooth": True,
|
||||
"showArea": False,
|
||||
"showSymbol": True,
|
||||
"symbolSize": 6,
|
||||
"lineWidth": 2,
|
||||
"showLegend": True,
|
||||
"legendPosition": "top",
|
||||
"colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"],
|
||||
"xAxisData": months,
|
||||
"seriesData": series_data
|
||||
}
|
||||
}
|
||||
|
||||
def _generate_bar_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]:
|
||||
"""生成柱状图"""
|
||||
module_name = module.get('module_name', '销售统计')
|
||||
fields = module.get('moduld_fields', [])
|
||||
|
||||
# 生成模拟数据
|
||||
regions = ["华东", "华南", "华北", "华中", "西南", "西北", "东北"]
|
||||
series_data = []
|
||||
|
||||
# 根据字段生成系列(最多2个)
|
||||
for i, field in enumerate(fields[:2]):
|
||||
data = [random.randint(200, 500) for _ in range(7)]
|
||||
series_data.append({
|
||||
"name": field if isinstance(field, str) else f"{2023+i}",
|
||||
"data": data
|
||||
})
|
||||
|
||||
# 如果没有字段,使用默认系列
|
||||
if not series_data:
|
||||
series_data = [
|
||||
{"name": "2023", "data": [320, 302, 301, 334, 390, 330, 320]},
|
||||
{"name": "2024", "data": [420, 382, 391, 434, 490, 430, 420]}
|
||||
]
|
||||
|
||||
bar_id = str(uuid4())
|
||||
return {
|
||||
"id": bar_id,
|
||||
"i": bar_id,
|
||||
"type": "chart-bar",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"w": 8,
|
||||
"h": 6,
|
||||
"minW": 4,
|
||||
"minH": 3,
|
||||
"title": "柱状图",
|
||||
"props": {
|
||||
"title": module_name,
|
||||
"horizontal": False,
|
||||
"stack": False,
|
||||
"barWidth": "auto",
|
||||
"barRadius": 4,
|
||||
"showBackground": False,
|
||||
"showLegend": True,
|
||||
"legendPosition": "top",
|
||||
"colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"],
|
||||
"xAxisData": regions,
|
||||
"seriesData": series_data
|
||||
}
|
||||
}
|
||||
|
||||
def _generate_pie_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]:
|
||||
"""生成饼图"""
|
||||
module_name = module.get('module_name', '流量来源')
|
||||
fields = module.get('moduld_fields', [])
|
||||
|
||||
# 根据字段生成数据
|
||||
series_data = []
|
||||
if fields and len(fields) > 0:
|
||||
# 使用字段名作为分类(最多5个)
|
||||
for field in fields[:5]:
|
||||
series_data.append({
|
||||
"name": field if isinstance(field, str) else f"分类{len(series_data)+1}",
|
||||
"value": random.randint(300, 1000)
|
||||
})
|
||||
|
||||
# 如果没有字段,使用默认数据
|
||||
if not series_data:
|
||||
series_data = [
|
||||
{"name": "搜索引擎", "value": 1048},
|
||||
{"name": "直接访问", "value": 735},
|
||||
{"name": "邮件营销", "value": 580},
|
||||
{"name": "联盟广告", "value": 484},
|
||||
{"name": "视频广告", "value": 300}
|
||||
]
|
||||
|
||||
pie_id = str(uuid4())
|
||||
return {
|
||||
"id": pie_id,
|
||||
"i": pie_id,
|
||||
"type": "chart-pie",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"w": 4,
|
||||
"h": 6,
|
||||
"minW": 3,
|
||||
"minH": 3,
|
||||
"title": "饼图",
|
||||
"props": {
|
||||
"title": module_name,
|
||||
"pieType": "rose",
|
||||
"radius": ["0%", "70%"],
|
||||
"showLabel": True,
|
||||
"labelPosition": "outside",
|
||||
"showLegend": True,
|
||||
"legendPosition": "bottom",
|
||||
"colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"],
|
||||
"seriesData": series_data
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user