""" 仪表盘发布节点 将仪表盘发布到菜单系统 """ 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)}', )