502 lines
18 KiB
Python
502 lines
18 KiB
Python
"""
|
|
系统总结节点
|
|
|
|
收集和展示AI创建的完整系统信息,包括应用、表单模块和仪表盘
|
|
"""
|
|
import ast
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from ..base import BaseNode, NodeContext, NodeResult
|
|
from ..registry import NodeRegistry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@NodeRegistry.register
|
|
class SystemSummaryNode(BaseNode):
|
|
"""
|
|
系统总结节点
|
|
|
|
收集和展示AI创建的完整系统信息,支持以下场景:
|
|
1. 完整应用创建:应用 + 多个表单模块 + 仪表盘
|
|
2. 子应用内创建模块:仅多个表单模块
|
|
3. 单个表单创建:单个表单模块
|
|
"""
|
|
|
|
node_type = 'system_summary'
|
|
node_name = '系统总结'
|
|
node_category = 'application'
|
|
node_icon = 'clipboard-check'
|
|
node_description = '收集和展示AI创建的完整系统信息'
|
|
|
|
inputs = [
|
|
{
|
|
'name': 'app_info',
|
|
'type': 'object',
|
|
'description': '应用信息(来自AppCreate节点,可选)',
|
|
'required': False,
|
|
},
|
|
{
|
|
'name': 'form_results',
|
|
'type': 'array',
|
|
'description': '表单结果列表(来自循环节点或单个表单发布节点)',
|
|
'required': True,
|
|
},
|
|
{
|
|
'name': 'dashboard_info',
|
|
'type': 'object',
|
|
'description': '仪表盘信息(来自DashboardPublish节点,可选)',
|
|
'required': False,
|
|
},
|
|
{
|
|
'name': 'summary_title',
|
|
'type': 'string',
|
|
'description': '总结标题',
|
|
'required': False,
|
|
},
|
|
]
|
|
|
|
outputs = [
|
|
{
|
|
'name': 'summary',
|
|
'type': 'object',
|
|
'description': '完整的总结数据',
|
|
},
|
|
{
|
|
'name': 'total_forms',
|
|
'type': 'number',
|
|
'description': '表单总数',
|
|
},
|
|
{
|
|
'name': 'has_app',
|
|
'type': 'boolean',
|
|
'description': '是否包含应用',
|
|
},
|
|
{
|
|
'name': 'has_dashboard',
|
|
'type': 'boolean',
|
|
'description': '是否包含仪表盘',
|
|
},
|
|
]
|
|
|
|
def execute(self, context: NodeContext) -> NodeResult:
|
|
"""执行节点"""
|
|
try:
|
|
# 检查是否有用户确认(点击完成按钮)
|
|
user_input = context.variables.get('__user_input__')
|
|
if user_input:
|
|
# 用户已确认,直接返回成功
|
|
logger.info('SystemSummaryNode - 用户已确认,完成展示')
|
|
context.variables.pop('__user_input__', None)
|
|
|
|
# 从用户输入中获取之前构建的总结数据
|
|
summary_data = self._parse_user_input(user_input)
|
|
if summary_data:
|
|
return self._build_final_result(summary_data)
|
|
|
|
# 获取输入参数
|
|
app_info_raw = self.config.get('app_info', '')
|
|
form_results_raw = self.config.get('form_results', '')
|
|
dashboard_info_raw = self.config.get('dashboard_info', '')
|
|
summary_title = context.resolve_template(self.config.get('summary_title', ''))
|
|
show_statistics = self.config.get('show_statistics', True)
|
|
base_url = context.resolve_template(self.config.get('base_url', ''))
|
|
|
|
# 解析变量引用
|
|
app_info = self._resolve_variable_value(context, app_info_raw)
|
|
form_results = self._resolve_variable_value(context, form_results_raw)
|
|
dashboard_info = self._resolve_variable_value(context, dashboard_info_raw)
|
|
|
|
logger.info(f'SystemSummaryNode - 开始构建系统总结')
|
|
logger.info(f'SystemSummaryNode - app_info: {bool(app_info)}, form_results: {type(form_results)}, dashboard_info: {bool(dashboard_info)}')
|
|
|
|
# 处理表单结果
|
|
forms = self._process_form_results(form_results)
|
|
|
|
# 处理应用信息
|
|
app = self._process_app_info(app_info) if app_info else None
|
|
|
|
# 处理仪表盘信息
|
|
dashboard = self._process_dashboard_info(dashboard_info) if dashboard_info else None
|
|
|
|
# 构建总结数据
|
|
summary = self._build_summary(
|
|
title=summary_title or '系统创建完成',
|
|
app=app,
|
|
forms=forms,
|
|
dashboard=dashboard,
|
|
show_statistics=show_statistics,
|
|
base_url=base_url,
|
|
)
|
|
|
|
logger.info(f'SystemSummaryNode - 总结构建完成: {len(forms)} 个表单')
|
|
|
|
# 返回等待用户确认的结果
|
|
return NodeResult(
|
|
success=True,
|
|
output=summary,
|
|
output_variables={
|
|
'summary': summary,
|
|
'total_forms': len(forms),
|
|
'has_app': app is not None,
|
|
'has_dashboard': dashboard is not None,
|
|
},
|
|
waiting_for_input=True,
|
|
waiting_config={
|
|
'type': 'design_preview',
|
|
'preview_type': 'system_summary',
|
|
'title': summary.get('title', '系统创建完成'),
|
|
'data': summary,
|
|
'editable': False,
|
|
},
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.exception(f'SystemSummaryNode - 执行失败: {e}')
|
|
return NodeResult(
|
|
success=False,
|
|
error=f'系统总结节点执行失败: {str(e)}',
|
|
)
|
|
|
|
def _parse_user_input(self, user_input: Any) -> Optional[Dict[str, Any]]:
|
|
"""解析用户输入"""
|
|
if isinstance(user_input, dict):
|
|
return user_input
|
|
|
|
if isinstance(user_input, str):
|
|
try:
|
|
return json.loads(user_input)
|
|
except (json.JSONDecodeError, TypeError):
|
|
pass
|
|
|
|
return None
|
|
|
|
def _resolve_variable_value(self, context: NodeContext, value: Any) -> Any:
|
|
"""解析变量引用,保持原始类型"""
|
|
import re
|
|
|
|
# 如果已经是字典或列表,直接返回
|
|
if isinstance(value, (dict, list)):
|
|
return value
|
|
|
|
# 如果不是字符串,返回原值
|
|
if not isinstance(value, str):
|
|
return value
|
|
|
|
value = value.strip()
|
|
if not value or value in ['{}', '[]', '']:
|
|
return None
|
|
|
|
# 检查是否是单个变量引用 {{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
|
|
try:
|
|
return json.loads(value)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# 尝试 Python literal_eval
|
|
try:
|
|
result = ast.literal_eval(value)
|
|
return result
|
|
except (ValueError, SyntaxError):
|
|
pass
|
|
|
|
return value
|
|
|
|
def _process_form_results(self, form_results: Any) -> List[Dict[str, Any]]:
|
|
"""处理表单结果列表"""
|
|
forms = []
|
|
|
|
if not form_results:
|
|
return forms
|
|
|
|
# 如果是单个表单结果(字典),包装成列表
|
|
if isinstance(form_results, dict):
|
|
form_results = [form_results]
|
|
|
|
if not isinstance(form_results, list):
|
|
logger.warning(f'form_results 类型不正确: {type(form_results)}')
|
|
return forms
|
|
|
|
for i, result in enumerate(form_results):
|
|
if not isinstance(result, dict):
|
|
continue
|
|
|
|
# 尝试从不同的数据结构中提取表单信息
|
|
form_info = self._extract_form_info(result, i)
|
|
if form_info:
|
|
forms.append(form_info)
|
|
|
|
return forms
|
|
|
|
def _extract_form_info(self, result: Dict[str, Any], index: int) -> Optional[Dict[str, Any]]:
|
|
"""从结果中提取表单信息"""
|
|
# 直接是表单发布节点的输出
|
|
if 'form_id' in result or 'menu_id' in result:
|
|
return {
|
|
'id': result.get('form_id', ''),
|
|
'name': result.get('form_name', result.get('menu_name', f'表单{index + 1}')),
|
|
'code': result.get('form_code', ''),
|
|
'description': result.get('description', ''),
|
|
'link': result.get('route_path', ''),
|
|
'menu_id': result.get('menu_id', ''),
|
|
'icon': result.get('menu_icon', 'lucide:file-text'),
|
|
}
|
|
|
|
# 循环节点的迭代结果(包含多个节点的输出)
|
|
# 尝试从 form_publish 或 form_create 节点输出中提取
|
|
for key in ['form_publish', 'form_create']:
|
|
if key in result and isinstance(result[key], dict):
|
|
sub_result = result[key]
|
|
return {
|
|
'id': sub_result.get('form_id', ''),
|
|
'name': sub_result.get('form_name', sub_result.get('menu_name', f'表单{index + 1}')),
|
|
'code': sub_result.get('form_code', ''),
|
|
'description': sub_result.get('description', ''),
|
|
'link': sub_result.get('route_path', ''),
|
|
'menu_id': sub_result.get('menu_id', ''),
|
|
'icon': sub_result.get('menu_icon', 'lucide:file-text'),
|
|
}
|
|
|
|
# 尝试从嵌套的 publish_result 中提取
|
|
if 'publish_result' in result and isinstance(result['publish_result'], dict):
|
|
pub = result['publish_result']
|
|
return {
|
|
'id': pub.get('form_id', result.get('form_id', '')),
|
|
'name': pub.get('form_name', pub.get('menu_name', f'表单{index + 1}')),
|
|
'code': pub.get('form_code', result.get('form_code', '')),
|
|
'description': pub.get('description', ''),
|
|
'link': pub.get('route_path', ''),
|
|
'menu_id': pub.get('menu_id', ''),
|
|
'icon': pub.get('menu_icon', 'lucide:file-text'),
|
|
}
|
|
|
|
# 尝试从 _node_xxx 格式的输出中提取
|
|
for key, value in result.items():
|
|
if key.startswith('_node_') and isinstance(value, dict):
|
|
if 'form_id' in value or 'route_path' in value:
|
|
return {
|
|
'id': value.get('form_id', ''),
|
|
'name': value.get('form_name', value.get('menu_name', f'表单{index + 1}')),
|
|
'code': value.get('form_code', ''),
|
|
'description': value.get('description', ''),
|
|
'link': value.get('route_path', ''),
|
|
'menu_id': value.get('menu_id', ''),
|
|
'icon': value.get('menu_icon', 'lucide:file-text'),
|
|
}
|
|
|
|
return None
|
|
|
|
def _process_app_info(self, app_info: Any) -> Optional[Dict[str, Any]]:
|
|
"""处理应用信息 - 只需要名称"""
|
|
if not app_info:
|
|
return None
|
|
|
|
# 如果是字符串,直接作为名称
|
|
if isinstance(app_info, str):
|
|
app_name = app_info.strip()
|
|
if not app_name:
|
|
return None
|
|
return {
|
|
'name': app_name,
|
|
'icon': 'lucide:app-window',
|
|
'link': '/', # 应用的基础URL
|
|
}
|
|
|
|
# 如果是字典,尝试提取名称
|
|
if isinstance(app_info, dict):
|
|
app_name = app_info.get('app_name') or app_info.get('name', '')
|
|
if not app_name:
|
|
return None
|
|
return {
|
|
'name': app_name,
|
|
'icon': app_info.get('icon', 'lucide:app-window'),
|
|
'link': '/',
|
|
}
|
|
|
|
return None
|
|
|
|
def _process_dashboard_info(self, dashboard_info: Any) -> Optional[Dict[str, Any]]:
|
|
"""处理仪表盘信息 - 只需要名称和路径"""
|
|
if not dashboard_info:
|
|
return None
|
|
|
|
# 如果是字符串,直接作为名称
|
|
if isinstance(dashboard_info, str):
|
|
dashboard_name = dashboard_info.strip()
|
|
if not dashboard_name:
|
|
return None
|
|
return {
|
|
'name': dashboard_name,
|
|
'icon': 'lucide:layout-dashboard',
|
|
'link': '', # 没有路径信息
|
|
}
|
|
|
|
# 如果是字典,提取名称和路径
|
|
if isinstance(dashboard_info, dict):
|
|
dashboard_name = (
|
|
dashboard_info.get('dashboard_name') or
|
|
dashboard_info.get('menu_name') or
|
|
dashboard_info.get('name', '')
|
|
)
|
|
if not dashboard_name:
|
|
return None
|
|
|
|
return {
|
|
'name': dashboard_name,
|
|
'icon': dashboard_info.get('menu_icon', 'lucide:layout-dashboard'),
|
|
'link': dashboard_info.get('route_path', ''),
|
|
}
|
|
|
|
return None
|
|
|
|
def _build_summary(
|
|
self,
|
|
title: str,
|
|
app: Optional[Dict[str, Any]],
|
|
forms: List[Dict[str, Any]],
|
|
dashboard: Optional[Dict[str, Any]],
|
|
show_statistics: bool,
|
|
base_url: str,
|
|
) -> Dict[str, Any]:
|
|
"""构建总结数据"""
|
|
# 计算统计信息
|
|
total_modules = len(forms)
|
|
if app:
|
|
total_modules += 1
|
|
if dashboard:
|
|
total_modules += 1
|
|
|
|
summary = {
|
|
'title': title,
|
|
'created_at': datetime.now().isoformat(),
|
|
'statistics': {
|
|
'total_modules': total_modules,
|
|
'forms_count': len(forms),
|
|
'has_app': app is not None,
|
|
'has_dashboard': dashboard is not None,
|
|
} if show_statistics else None,
|
|
'app': app,
|
|
'forms': forms,
|
|
'dashboard': dashboard,
|
|
'base_url': base_url,
|
|
}
|
|
|
|
return summary
|
|
|
|
def _build_final_result(self, summary_data: Dict[str, Any]) -> NodeResult:
|
|
"""构建最终结果(用户确认后)"""
|
|
forms = summary_data.get('forms', [])
|
|
app = summary_data.get('app')
|
|
dashboard = summary_data.get('dashboard')
|
|
|
|
return NodeResult(
|
|
success=True,
|
|
output=summary_data,
|
|
output_variables={
|
|
'summary': summary_data,
|
|
'total_forms': len(forms),
|
|
'has_app': app is not None,
|
|
'has_dashboard': dashboard is not None,
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def get_config_schema(cls) -> Dict[str, Any]:
|
|
"""获取节点配置 Schema"""
|
|
return {
|
|
'type': 'object',
|
|
'properties': {
|
|
'app_info': {
|
|
'type': 'string',
|
|
'title': '应用信息',
|
|
'description': '应用信息(来自AppCreate节点),支持变量引用如 {{app_create.app_id}}',
|
|
'required': False,
|
|
'x-component': 'SmartInput',
|
|
'x-component-props': {
|
|
'placeholder': '留空则不显示应用信息',
|
|
},
|
|
},
|
|
'form_results': {
|
|
'type': 'string',
|
|
'title': '表单结果列表',
|
|
'description': '表单结果列表(来自循环节点或单个表单发布节点),支持变量引用如 {{loop.results}}',
|
|
'required': True,
|
|
'x-component': 'SmartInput',
|
|
'x-component-props': {
|
|
'placeholder': '{{loop.results}} 或 {{form_publish.publish_result}}',
|
|
},
|
|
},
|
|
'dashboard_info': {
|
|
'type': 'string',
|
|
'title': '仪表盘信息',
|
|
'description': '仪表盘信息(来自DashboardPublish节点),支持变量引用',
|
|
'required': False,
|
|
'x-component': 'SmartInput',
|
|
'x-component-props': {
|
|
'placeholder': '留空则不显示仪表盘信息',
|
|
},
|
|
},
|
|
'summary_title': {
|
|
'type': 'string',
|
|
'title': '总结标题',
|
|
'description': '总结页面的标题',
|
|
'default': '系统创建完成',
|
|
'x-component': 'Input',
|
|
'x-component-props': {
|
|
'placeholder': '系统创建完成',
|
|
},
|
|
},
|
|
'show_statistics': {
|
|
'type': 'boolean',
|
|
'title': '显示统计信息',
|
|
'description': '是否显示模块数量等统计信息',
|
|
'default': True,
|
|
'x-component': 'Switch',
|
|
},
|
|
'base_url': {
|
|
'type': 'string',
|
|
'title': '基础URL',
|
|
'description': '用于生成完整链接的基础URL(可选)',
|
|
'required': False,
|
|
'x-component': 'Input',
|
|
'x-component-props': {
|
|
'placeholder': '留空则使用相对路径',
|
|
},
|
|
},
|
|
},
|
|
'required': ['form_results'],
|
|
}
|