Restore AI workflow design nodes
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
仪表盘发布节点
|
||||
|
||||
将仪表盘发布到菜单系统
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..base import BaseNode, NodeContext, NodeResult
|
||||
from ..registry import NodeRegistry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@NodeRegistry.register
|
||||
class DashboardPublishNode(BaseNode):
|
||||
"""
|
||||
仪表盘发布节点
|
||||
|
||||
将仪表盘发布到菜单系统,创建菜单项
|
||||
"""
|
||||
|
||||
node_type = 'dashboard_publish'
|
||||
node_name = '仪表盘发布'
|
||||
node_category = 'dashboard'
|
||||
node_icon = 'upload'
|
||||
node_description = '将仪表盘发布到菜单系统'
|
||||
|
||||
inputs = [
|
||||
{
|
||||
'name': 'dashboard_id',
|
||||
'type': 'string',
|
||||
'description': '仪表盘ID(来自创建节点)',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'menu_name',
|
||||
'type': 'string',
|
||||
'description': '菜单名称',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'menu_parent_id',
|
||||
'type': 'string',
|
||||
'description': '父菜单ID(可选)',
|
||||
'required': False,
|
||||
},
|
||||
{
|
||||
'name': 'menu_icon',
|
||||
'type': 'string',
|
||||
'description': '菜单图标',
|
||||
'required': False,
|
||||
},
|
||||
{
|
||||
'name': 'menu_order',
|
||||
'type': 'number',
|
||||
'description': '菜单排序',
|
||||
'required': False,
|
||||
},
|
||||
{
|
||||
'name': 'application_id',
|
||||
'type': 'string',
|
||||
'description': '所属应用ID',
|
||||
'required': False,
|
||||
},
|
||||
]
|
||||
|
||||
outputs = [
|
||||
{
|
||||
'name': 'menu_id',
|
||||
'type': 'string',
|
||||
'description': '创建的菜单ID',
|
||||
},
|
||||
{
|
||||
'name': 'route_path',
|
||||
'type': 'string',
|
||||
'description': '访问路径',
|
||||
},
|
||||
{
|
||||
'name': 'publish_result',
|
||||
'type': 'object',
|
||||
'description': '发布结果',
|
||||
},
|
||||
]
|
||||
|
||||
def execute(self, context: NodeContext) -> NodeResult:
|
||||
"""执行节点(同步方法,通过运行异步方法实现)"""
|
||||
import asyncio
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
import concurrent.futures
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future = executor.submit(asyncio.run, self.execute_async(context))
|
||||
return future.result()
|
||||
else:
|
||||
return loop.run_until_complete(self.execute_async(context))
|
||||
|
||||
async def execute_async(self, context: NodeContext) -> NodeResult:
|
||||
"""异步执行节点"""
|
||||
try:
|
||||
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
|
||||
require_confirmation = self.resolve_require_confirmation(context, default=True)
|
||||
|
||||
# 检查是否有用户输入(确认后的数据)
|
||||
user_input = context.user_input
|
||||
logger.info(f'DashboardPublishNode - user_input 类型: {type(user_input)}, 值: {user_input}')
|
||||
logger.info(f'DashboardPublishNode - require_confirmation: {require_confirmation}')
|
||||
|
||||
# 解析 user_input(可能是字符串或字典)
|
||||
user_input_dict = None
|
||||
if user_input:
|
||||
if isinstance(user_input, str):
|
||||
try:
|
||||
import json
|
||||
user_input_dict = json.loads(user_input)
|
||||
logger.info(f'DashboardPublishNode - 解析 JSON 字符串为字典: {user_input_dict}')
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f'DashboardPublishNode - JSON 解析失败: {e}')
|
||||
elif isinstance(user_input, dict):
|
||||
user_input_dict = user_input
|
||||
|
||||
# 检查 user_input 是否包含发布节点需要的字段(dashboard_id 和 menu_name)
|
||||
# 如果包含,说明是用户确认后的数据;否则是从上一个节点传递过来的数据
|
||||
if user_input_dict and 'dashboard_id' in user_input_dict and 'menu_name' in user_input_dict:
|
||||
# 用户已确认,使用确认后的数据
|
||||
logger.info(f'DashboardPublishNode - 使用用户确认的发布配置')
|
||||
return await self._execute_publish(context, user_input_dict)
|
||||
|
||||
# 获取输入参数
|
||||
dashboard_id = context.resolve_template(self.config.get('dashboard_id', ''))
|
||||
menu_name = context.resolve_template(self.config.get('menu_name', ''))
|
||||
menu_parent_id = context.resolve_template(self.config.get('menu_parent_id', ''))
|
||||
menu_icon = context.resolve_template(self.config.get('menu_icon', 'lucide:layout-dashboard'))
|
||||
application_id = context.resolve_template(self.config.get('application_id', ''))
|
||||
|
||||
# 处理menu_order
|
||||
menu_order_value = self.config.get('menu_order', 0)
|
||||
menu_order_str = context.resolve_template(str(menu_order_value))
|
||||
try:
|
||||
menu_order = int(menu_order_str) if menu_order_str else 0
|
||||
except (ValueError, TypeError):
|
||||
menu_order = 0
|
||||
|
||||
logger.info(f'DashboardPublishNode - 开始发布仪表盘: {dashboard_id}')
|
||||
|
||||
# 验证必要参数
|
||||
if not dashboard_id:
|
||||
return NodeResult(
|
||||
success=False,
|
||||
error='仪表盘ID不能为空',
|
||||
)
|
||||
|
||||
if not menu_name:
|
||||
return NodeResult(
|
||||
success=False,
|
||||
error='菜单名称不能为空',
|
||||
)
|
||||
|
||||
# 获取仪表盘的 code(用于显示路由路径)
|
||||
dashboard_code = ''
|
||||
try:
|
||||
from app.database import AsyncSessionLocal
|
||||
from online_dev.page_manager.model import PageMeta
|
||||
from sqlalchemy import select
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
stmt = select(PageMeta.code).where(PageMeta.id == dashboard_id, PageMeta.is_deleted == False)
|
||||
result = await db.execute(stmt)
|
||||
dashboard_code = result.scalar_one_or_none() or ''
|
||||
except Exception as e:
|
||||
logger.warning(f'获取仪表盘 code 失败: {e}')
|
||||
|
||||
# 构建发布数据
|
||||
publish_data = {
|
||||
'dashboard_id': dashboard_id,
|
||||
'dashboard_code': dashboard_code,
|
||||
'menu_name': menu_name,
|
||||
'menu_parent_id': menu_parent_id,
|
||||
'menu_icon': menu_icon,
|
||||
'menu_order': menu_order,
|
||||
'application_id': application_id,
|
||||
}
|
||||
|
||||
# 如果需要确认,返回等待用户输入
|
||||
if require_confirmation:
|
||||
return NodeResult(
|
||||
success=True,
|
||||
output=publish_data,
|
||||
output_variables=publish_data,
|
||||
waiting_for_input=True,
|
||||
waiting_config={
|
||||
'type': 'design_preview',
|
||||
'preview_type': 'dashboard_publish',
|
||||
'title': '仪表盘发布确认',
|
||||
'data': publish_data,
|
||||
'editable': True,
|
||||
},
|
||||
)
|
||||
|
||||
# 不需要确认,直接发布
|
||||
return await self._execute_publish(context, publish_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'DashboardPublishNode - 执行失败: {str(e)}')
|
||||
return NodeResult(
|
||||
success=False,
|
||||
error=f'发布仪表盘失败: {str(e)}',
|
||||
)
|
||||
|
||||
async def _execute_publish(self, context: NodeContext, publish_data: Dict[str, Any]) -> NodeResult:
|
||||
"""执行发布操作"""
|
||||
try:
|
||||
dashboard_id = publish_data.get('dashboard_id', '')
|
||||
menu_name = publish_data.get('menu_name', '')
|
||||
menu_parent_id = publish_data.get('menu_parent_id', '')
|
||||
menu_icon = publish_data.get('menu_icon', 'lucide:layout-dashboard')
|
||||
menu_order = publish_data.get('menu_order', 0)
|
||||
application_id = publish_data.get('application_id', '')
|
||||
|
||||
if not dashboard_id or not menu_name:
|
||||
return NodeResult(
|
||||
success=False,
|
||||
error='仪表盘ID和菜单名称不能为空',
|
||||
)
|
||||
|
||||
# 发布到数据库
|
||||
from app.database import AsyncSessionLocal
|
||||
from online_dev.page_manager.model import PageMeta
|
||||
from core.menu.model import Menu
|
||||
from sqlalchemy import select
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
# 获取仪表盘信息
|
||||
stmt = select(PageMeta).where(PageMeta.id == dashboard_id, PageMeta.is_deleted == False)
|
||||
result = await db.execute(stmt)
|
||||
page = result.scalar_one_or_none()
|
||||
|
||||
if not page:
|
||||
return NodeResult(
|
||||
success=False,
|
||||
error=f'仪表盘不存在: {dashboard_id}',
|
||||
)
|
||||
|
||||
# 生成路由路径
|
||||
route_path = f'/page-render/{page.code}'
|
||||
|
||||
# 检查菜单是否已存在
|
||||
menu_stmt = select(Menu).where(
|
||||
Menu.path == route_path,
|
||||
Menu.is_deleted == False
|
||||
)
|
||||
menu_result = await db.execute(menu_stmt)
|
||||
existing_menu = menu_result.scalar_one_or_none()
|
||||
|
||||
if existing_menu:
|
||||
# 更新现有菜单
|
||||
existing_menu.title = menu_name
|
||||
existing_menu.icon = menu_icon
|
||||
existing_menu.order = menu_order
|
||||
if menu_parent_id:
|
||||
existing_menu.parent_id = menu_parent_id
|
||||
|
||||
menu_id = existing_menu.id
|
||||
logger.info(f'DashboardPublishNode - 更新菜单: {menu_id}')
|
||||
else:
|
||||
# 创建新菜单
|
||||
new_menu = Menu(
|
||||
name=menu_name,
|
||||
title=menu_name,
|
||||
path=route_path,
|
||||
component='_core/page-render/index',
|
||||
icon=menu_icon,
|
||||
order=menu_order,
|
||||
type='online_page',
|
||||
parent_id=menu_parent_id if menu_parent_id else None,
|
||||
application_id=application_id if application_id else page.application_id,
|
||||
)
|
||||
|
||||
db.add(new_menu)
|
||||
await db.flush()
|
||||
menu_id = new_menu.id
|
||||
logger.info(f'DashboardPublishNode - 创建菜单: {menu_id}')
|
||||
|
||||
# 更新页面状态为已发布
|
||||
page.status = 'published'
|
||||
|
||||
await db.commit()
|
||||
|
||||
output_data = {
|
||||
'menu_id': menu_id,
|
||||
'route_path': route_path,
|
||||
'dashboard_id': dashboard_id,
|
||||
'dashboard_code': page.code,
|
||||
'dashboard_name': page.name,
|
||||
'menu_name': menu_name,
|
||||
'status': 'published',
|
||||
}
|
||||
|
||||
return NodeResult(
|
||||
success=True,
|
||||
output=output_data,
|
||||
output_variables=output_data,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'DashboardPublishNode - 执行失败: {str(e)}')
|
||||
return NodeResult(
|
||||
success=False,
|
||||
error=f'发布仪表盘失败: {str(e)}',
|
||||
)
|
||||
Reference in New Issue
Block a user