339 lines
12 KiB
Python
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,
|
|
}
|