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

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'],
}