Restore AI workflow design nodes

This commit is contained in:
2026-06-09 21:18:33 +08:00
parent fbd726ebdf
commit cb45c0a068
71 changed files with 22676 additions and 21 deletions
@@ -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)}',
)