Files
ai-agent-admin/backend-fastapi/ai_platform/nodes/builtin/dashboard_publish_node.py
T
2026-06-09 21:18:33 +08:00

312 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
仪表盘发布节点
将仪表盘发布到菜单系统
"""
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)}',
)