""" 应用设置节点 接收应用设置配置,展示给用户确认或编辑 包括:应用配置、Logo配置、内置主题和布局 """ import json import logging from typing import Any, Dict, Optional from ..base import BaseNode, NodeContext, NodeResult from ..registry import NodeRegistry logger = logging.getLogger(__name__) @NodeRegistry.register class AppSettingsNode(BaseNode): """ 应用设置节点 接收应用设置配置,展示给用户确认或编辑 包括:应用配置、Logo配置、内置主题和布局 """ node_type = 'app_settings' node_name = '应用设置' node_category = 'application' node_icon = 'settings' node_description = '配置应用设置,包括应用信息、Logo、主题和布局' inputs = [ { 'name': 'app_name', 'type': 'string', 'description': '应用名称', 'required': False, }, { 'name': 'home_path', 'type': 'string', 'description': '首页路径', 'required': False, }, { 'name': 'logo_source', 'type': 'string', 'description': 'Logo图片URL或文件ID', 'required': False, }, { 'name': 'theme_builtin_type', 'type': 'string', 'description': '内置主题类型', 'required': False, }, { 'name': 'theme_color_primary', 'type': 'string', 'description': '主题主色调', 'required': False, }, { 'name': 'app_layout', 'type': 'string', 'description': '应用布局类型', 'required': False, }, ] outputs = [ { 'name': 'settings', 'type': 'object', 'description': '应用设置配置', }, { 'name': 'confirmed', 'type': 'boolean', 'description': '是否已确认', }, ] # 默认配置 DEFAULT_SETTINGS = { 'app': { 'name': '', 'locale': 'zh-CN', 'dynamicTitle': True, 'watermark': False, 'watermarkContent': '', 'enableCheckUpdates': True, 'defaultHomePath': '/analytics', 'enablePreferences': True, }, 'logo': { 'enable': True, 'source': '', 'fit': 'contain', }, 'theme': { 'colorPrimary': 'hsl(212 100% 45%)', 'builtinType': 'default', 'mode': 'light', 'radius': '0.5', 'semiDarkSidebar': False, 'semiDarkHeader': False, }, 'sidebar': { 'enable': True, 'width': 230, 'collapsed': False, 'collapsedShowTitle': False, 'autoActivateChild': False, 'expandOnHover': True, 'collapsedButton': True, 'fixedButton': True, }, 'header': { 'enable': True, 'mode': 'fixed', 'menuAlign': 'start', }, 'footer': { 'enable': True, 'fixed': True, }, 'copyright': { 'enable': True, 'companyName': '', 'companySiteLink': '', 'date': '', 'icp': '', 'icpLink': '', }, 'navigation': { 'styleType': 'rounded', 'split': True, 'accordion': True, }, 'tabbar': { 'enable': True, 'showIcon': True, 'showMore': True, 'showMaximize': True, 'persist': True, 'draggable': True, 'wheelable': True, 'styleType': 'chrome', 'maxCount': 0, 'middleClickToClose': True, }, 'breadcrumb': { 'enable': True, 'showIcon': True, 'showHome': True, 'styleType': 'normal', 'hideOnlyOne': False, }, 'transition': { 'progress': True, 'name': 'fade-slide', 'loading': True, 'enable': True, }, 'widget': { 'globalSearch': True, 'fullscreen': True, 'languageToggle': True, 'notification': True, 'themeToggle': True, 'sidebarToggle': True, 'lockScreen': True, 'refresh': True, }, 'shortcutKeys': { 'enable': True, 'globalSearch': True, 'globalLogout': True, 'globalLockScreen': True, }, } # 布局类型选项 LAYOUT_OPTIONS = [ {'label': '侧边导航', 'value': 'sidebar-nav'}, {'label': '侧边混合导航', 'value': 'sidebar-mixed-nav'}, {'label': '顶部导航', 'value': 'header-nav'}, {'label': '混合导航', 'value': 'mixed-nav'}, {'label': '全屏内容', 'value': 'full-content'}, ] # 内置主题选项 BUILTIN_THEME_OPTIONS = [ {'label': '默认', 'value': 'default'}, {'label': '紫罗兰', 'value': 'violet'}, {'label': '粉红', 'value': 'pink'}, {'label': '玫瑰', 'value': 'rose'}, {'label': '天蓝', 'value': 'sky'}, {'label': '青色', 'value': 'cyan'}, {'label': '绿色', 'value': 'green'}, {'label': '橙色', 'value': 'orange'}, {'label': '黄色', 'value': 'yellow'}, {'label': '锌色', 'value': 'zinc'}, {'label': '中性', 'value': 'neutral'}, {'label': '石板', 'value': 'slate'}, {'label': '灰色', 'value': 'gray'}, {'label': '深灰', 'value': 'deep-blue'}, {'label': '深绿', 'value': 'deep-green'}, ] def execute(self, context: NodeContext) -> NodeResult: """执行节点""" try: # 检查是否有用户编辑的数据(从设计预览面板提交) user_input = context.variables.get('__user_input__') if user_input: edited_data = self._parse_user_edit(user_input) if edited_data: logger.info(f'AppSettingsNode - 使用用户编辑的数据') # 使用编辑后的数据,清除用户输入 context.variables.pop('__user_input__', None) return self._build_result(edited_data, require_confirmation=False) # 获取输入参数 app_name = context.resolve_template(self.config.get('app_name', '')) home_path = context.resolve_template(self.config.get('home_path', '')) logo_source = context.resolve_template(self.config.get('logo_source', '')) theme_builtin_type = context.resolve_template(self.config.get('theme_builtin_type', 'default')) theme_color_primary = context.resolve_template(self.config.get('theme_color_primary', '')) app_layout = context.resolve_template(self.config.get('app_layout', 'sidebar-nav')) logger.info(f'AppSettingsNode - 输入参数: app_name={app_name}, home_path={home_path}, layout={app_layout}') # 构建设置数据(合并默认值和输入值) settings = self._build_settings( app_name=app_name, home_path=home_path, logo_source=logo_source, theme_builtin_type=theme_builtin_type, theme_color_primary=theme_color_primary, app_layout=app_layout, ) logger.info(f'AppSettingsNode - 输出设置配置') # 检查是否需要确认(支持布尔值、字符串模式和变量引用) require_confirmation = self.resolve_require_confirmation(context, default=True) return self._build_result(settings, require_confirmation) except Exception as e: logger.exception(f'应用设置节点执行失败: {e}') return NodeResult( success=False, error=f'节点执行失败: {str(e)}', ) async def execute_async(self, context: NodeContext) -> NodeResult: """异步执行节点(直接调用同步方法)""" return self.execute(context) def _parse_user_edit(self, user_input: Any) -> Optional[Dict[str, Any]]: """解析用户编辑的数据""" if isinstance(user_input, dict): # 检查是否包含设置字段 if 'app' in user_input or 'theme' in user_input or 'logo' in user_input: return user_input return None if isinstance(user_input, str): try: data = json.loads(user_input) if isinstance(data, dict) and ('app' in data or 'theme' in data or 'logo' in data): return data except (json.JSONDecodeError, TypeError): pass return None def _build_settings( self, app_name: str = '', home_path: str = '', logo_source: str = '', theme_builtin_type: str = 'default', theme_color_primary: str = '', app_layout: str = 'sidebar-nav', ) -> Dict[str, Any]: """构建设置配置""" import copy settings = copy.deepcopy(self.DEFAULT_SETTINGS) # 应用配置 if app_name: settings['app']['name'] = app_name # 首页路径配置 if home_path: settings['app']['defaultHomePath'] = home_path # 布局配置 if app_layout: settings['app']['layout'] = app_layout # Logo配置 if logo_source: settings['logo']['source'] = logo_source # 主题配置 if theme_builtin_type: settings['theme']['builtinType'] = theme_builtin_type if theme_color_primary: settings['theme']['colorPrimary'] = theme_color_primary return settings def _build_result(self, settings: Dict[str, Any], require_confirmation: bool = False) -> NodeResult: """构建节点结果""" # 构建预览数据 preview_data = { 'type': 'app_settings', 'title': '应用设置', 'data': settings, 'editable': True, 'layoutOptions': self.LAYOUT_OPTIONS, 'themeOptions': self.BUILTIN_THEME_OPTIONS, } if require_confirmation else None return NodeResult( success=True, output=settings, output_variables={ 'settings': settings, 'confirmed': not require_confirmation, }, preview=preview_data, waiting_for_input=require_confirmation, waiting_config={ 'type': 'design_preview', 'preview_type': 'app_settings', 'title': '应用设置', 'message': '请确认或编辑应用设置', 'data': settings, 'layoutOptions': self.LAYOUT_OPTIONS, 'themeOptions': self.BUILTIN_THEME_OPTIONS, } if require_confirmation else {}, ) @classmethod def get_config_schema(cls) -> Dict[str, Any]: """ 获取节点配置 Schema(供前端表单渲染) Returns: 配置 Schema """ return { 'type': 'object', 'properties': { 'app_name': { 'type': 'string', 'title': '应用名称', 'description': '应用的显示名称,支持变量引用如 {{llm_output}}', 'required': False, 'x-component': 'Input', 'x-component-props': { 'placeholder': '请输入应用名称', }, }, 'home_path': { 'type': 'string', 'title': '首页路径', 'description': '应用首页路径,如 /dashboard 或 /analytics', 'required': False, 'x-component': 'Input', 'x-component-props': { 'placeholder': '如 /dashboard', }, }, 'logo_source': { 'type': 'string', 'title': 'Logo图片', 'description': 'Logo图片URL或文件ID,支持变量引用', 'required': False, 'x-component': 'Input', 'x-component-props': { 'placeholder': '请输入Logo图片URL', }, }, 'theme_builtin_type': { 'type': 'string', 'title': '内置主题', 'description': '选择内置主题类型', 'default': 'default', 'enum': ['default', 'violet', 'pink', 'rose', 'sky', 'cyan', 'green', 'orange', 'yellow', 'zinc', 'neutral', 'slate', 'gray', 'deep-blue', 'deep-green'], 'x-component': 'Select', 'x-component-props': { 'placeholder': '请选择内置主题', }, }, 'theme_color_primary': { 'type': 'string', 'title': '主题主色调', 'description': '主题主色调,如 hsl(212 100% 45%)', 'required': False, 'x-component': 'Input', 'x-component-props': { 'placeholder': '如 hsl(212 100% 45%)', }, }, 'app_layout': { 'type': 'string', 'title': '应用布局', 'description': '应用布局类型', 'default': 'sidebar-nav', 'enum': ['sidebar-nav', 'sidebar-mixed-nav', 'header-nav', 'mixed-nav', 'full-content'], 'x-component': 'Select', 'x-component-props': { 'placeholder': '请选择布局类型', }, }, 'require_confirmation': { 'type': 'boolean', 'title': '需要确认', 'description': '设置完成后是否暂停等待用户确认或编辑', 'default': True, 'x-component': 'Switch', }, }, 'required': [], }