""" 仪表盘创建节点 将仪表盘配置保存到数据库 """ 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, }