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

339 lines
12 KiB
Python

"""
仪表盘创建节点
将仪表盘配置保存到数据库
"""
import json
import logging
from typing import Any, Dict
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class DashboardCreateNode(BaseNode):
"""
仪表盘创建节点
将仪表盘基础信息和页面配置保存到数据库
"""
node_type = 'dashboard_create'
node_name = '仪表盘创建'
node_category = 'dashboard'
node_icon = 'file-plus'
node_description = '将仪表盘配置保存到数据库'
inputs = [
{
'name': 'dashboard_basic_info',
'type': 'object',
'description': '仪表盘基础信息(来自基础信息节点)',
'required': True,
},
{
'name': 'page_config',
'type': 'object',
'description': '页面设计配置(来自设计节点)',
'required': True,
},
{
'name': 'application_id',
'type': 'string',
'description': '所属应用ID(可选)',
'required': False,
},
]
outputs = [
{
'name': 'dashboard_id',
'type': 'string',
'description': '创建的仪表盘ID',
},
{
'name': 'dashboard_code',
'type': 'string',
'description': '仪表盘编码',
},
{
'name': 'page_meta',
'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:
# 获取输入参数
dashboard_basic_info_raw = self.config.get('dashboard_basic_info', '{}')
page_config_raw = self.config.get('page_config', '{}')
# 解析变量引用
dashboard_basic_info = self._resolve_variable_value(context, dashboard_basic_info_raw)
page_config = self._resolve_variable_value(context, page_config_raw)
application_id = context.resolve_template(self.config.get('application_id', ''))
# 配置选项
update_if_exists = self.config.get('update_if_exists', True)
logger.info(f'DashboardCreateNode - 开始创建仪表盘')
logger.info(f'DashboardCreateNode - 原始配置: dashboard_basic_info_raw={dashboard_basic_info_raw}, page_config_raw={page_config_raw}')
logger.info(f'DashboardCreateNode - 上下文变量: {list(context.variables.keys())}')
logger.info(f'DashboardCreateNode - dashboard_basic_info type: {type(dashboard_basic_info)}, value: {dashboard_basic_info}')
logger.info(f'DashboardCreateNode - page_config type: {type(page_config)}, value: {str(page_config)[:200]}')
# 确保 dashboard_basic_info 是字典
if isinstance(dashboard_basic_info, str):
try:
dashboard_basic_info = json.loads(dashboard_basic_info)
logger.info(f'DashboardCreateNode - 成功解析 dashboard_basic_info 字符串为字典')
except json.JSONDecodeError as e:
logger.error(f'DashboardCreateNode - 解析 dashboard_basic_info 失败: {e}')
return NodeResult(
success=False,
error=f'仪表盘基础信息格式错误: {str(e)}',
)
# 确保 page_config 是字典
if isinstance(page_config, str):
try:
page_config = json.loads(page_config)
logger.info(f'DashboardCreateNode - 成功解析 page_config 字符串为字典')
except json.JSONDecodeError as e:
logger.error(f'DashboardCreateNode - 解析 page_config 失败: {e}')
return NodeResult(
success=False,
error=f'页面配置格式错误: {str(e)}',
)
# 验证必要配置
if not dashboard_basic_info:
return NodeResult(
success=False,
error='仪表盘基础信息不能为空',
)
if not page_config:
return NodeResult(
success=False,
error='页面设计配置不能为空',
)
# 提取基础信息
name = dashboard_basic_info.get('name', '')
code_base = dashboard_basic_info.get('code', '')
category = dashboard_basic_info.get('category', 'dashboard')
description = dashboard_basic_info.get('description', '')
sort = dashboard_basic_info.get('sort', 0)
if not name or not code_base:
return NodeResult(
success=False,
error='仪表盘名称和编码不能为空',
)
# 保存到数据库
from app.database import AsyncSessionLocal
from online_dev.page_manager.model import PageMeta
from sqlalchemy import select
async with AsyncSessionLocal() as db:
# 生成唯一的 code(如果已存在则自动添加后缀)
code = await self._get_unique_code(db, code_base)
logger.info(f'DashboardCreateNode - 使用 code: {code} (原始: {code_base})')
# 检查是否已存在(理论上不应该存在,因为已经生成了唯一 code)
stmt = select(PageMeta).where(PageMeta.code == code, PageMeta.is_deleted == False)
result = await db.execute(stmt)
existing_page = result.scalar_one_or_none()
if existing_page:
if update_if_exists:
# 更新现有记录
existing_page.name = name
existing_page.category = category
existing_page.description = description
existing_page.sort = sort
existing_page.page_config = page_config
if application_id:
existing_page.application_id = application_id
existing_page.version = (existing_page.version or 1) + 1
await db.commit()
await db.refresh(existing_page)
page_meta = self._page_to_dict(existing_page)
logger.info(f'DashboardCreateNode - 更新仪表盘成功: {code}')
else:
return NodeResult(
success=False,
error=f'仪表盘编码已存在: {code}',
)
else:
# 创建新记录
new_page = PageMeta(
name=name,
code=code,
category=category,
description=description,
sort=sort,
status='draft',
version=1,
page_config=page_config,
application_id=application_id if application_id else None,
)
db.add(new_page)
await db.commit()
await db.refresh(new_page)
page_meta = self._page_to_dict(new_page)
logger.info(f'DashboardCreateNode - 创建仪表盘成功: {code}')
output_data = {
'dashboard_id': page_meta['id'],
'dashboard_code': page_meta['code'],
'page_meta': page_meta,
}
return NodeResult(
success=True,
output=output_data,
output_variables=output_data,
)
except Exception as e:
logger.error(f'DashboardCreateNode - 执行失败: {str(e)}')
return NodeResult(
success=False,
error=f'创建仪表盘失败: {str(e)}',
)
def _resolve_variable_value(self, context: NodeContext, value: Any) -> Any:
"""解析变量引用,保持原始类型"""
import re
import ast
# 如果已经是字典,直接返回
if isinstance(value, dict):
return value
# 如果不是字符串,返回原值
if not isinstance(value, str):
return value
value = value.strip()
if not value or value == '{}':
return {}
# 检查是否是单个变量引用 {{node_id.variable_name}}
single_var_pattern = r'^\{\{([^}]+)\}\}$'
match = re.match(single_var_pattern, value)
if match:
var_ref = match.group(1).strip()
# 解析 node_id.variable_name 格式
if '.' in var_ref:
parts = var_ref.split('.', 1)
node_id = parts[0]
key = parts[1]
# 从节点输出命名空间获取
node_outputs = context.variables.get(f'_node_{node_id}')
if isinstance(node_outputs, dict) and key in node_outputs:
return node_outputs[key]
# 直接从变量中获取
if key in context.variables:
return context.variables[key]
logger.warning(f'变量引用未找到: {var_ref}')
else:
# 直接变量引用
if var_ref in context.variables:
return context.variables[var_ref]
logger.warning(f'变量引用未找到: {var_ref}')
# 不是变量引用,尝试解析为 JSON 或 Python dict
# 尝试 JSON 解析
try:
return json.loads(value)
except json.JSONDecodeError:
pass
# 尝试 Python literal_eval(处理单引号格式)
try:
result = ast.literal_eval(value)
if isinstance(result, dict):
return result
return result
except (ValueError, SyntaxError) as e:
logger.warning(f'配置解析失败: {e}, 值: {value[:200]}')
return value
async def _get_unique_code(self, db, base_code: str) -> str:
"""
生成唯一的 code,如果已存在则自动添加数字后缀
Args:
db: 数据库会话
base_code: 基础 code
Returns:
唯一的 code
"""
from online_dev.page_manager import PageService
code = base_code
counter = 1
while True:
try:
existing = await PageService.get_by_code(db, code)
if not existing:
# code 不存在,可以使用
break
# code 已存在,添加后缀
code = f"{base_code}_{counter}"
counter += 1
except Exception:
# 查询出错,认为 code 可用
break
return code
def _page_to_dict(self, page: Any) -> Dict[str, Any]:
"""将PageMeta对象转换为字典"""
return {
'id': page.id,
'name': page.name,
'code': page.code,
'category': page.category,
'description': page.description,
'status': page.status,
'version': page.version,
'sort': page.sort,
'page_config': page.page_config,
'application_id': page.application_id,
}