""" 代码执行节点 """ import concurrent.futures import logging import time from typing import Any, Dict from ..base import BaseNode, NodeContext, NodeResult from ..registry import NodeRegistry logger = logging.getLogger(__name__) @NodeRegistry.register class CodeNode(BaseNode): """ 代码执行节点 执行 Python 代码片段 """ node_type = 'code' node_name = '代码' node_category = 'logic' node_icon = 'code' node_description = '执行 Python 代码片段' inputs = [ { 'name': 'inputs', 'type': 'object', 'description': '输入变量', }, ] outputs = [ { 'name': 'result', 'type': 'any', 'description': '执行结果', }, ] # 安全的内置函数白名单 SAFE_BUILTINS = { 'abs', 'all', 'any', 'bool', 'dict', 'enumerate', 'filter', 'float', 'int', 'len', 'list', 'map', 'max', 'min', 'range', 'round', 'set', 'sorted', 'str', 'sum', 'tuple', 'zip', 'True', 'False', 'None', } def execute(self, context: NodeContext) -> NodeResult: """执行代码(线程池隔离,防止死循环卡死主流程)""" timeout = self.config.get('timeout', 30) with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: future = executor.submit(self._execute_sync, context) try: return future.result(timeout=timeout) except concurrent.futures.TimeoutError: future.cancel() return NodeResult( success=False, error=f'代码执行超时({timeout}秒),请检查是否存在死循环', elapsed_time=timeout * 1000, ) def _execute_sync(self, context: NodeContext) -> NodeResult: """同步执行代码(在子线程中运行)""" start_time = time.time() try: code = self.config.get('code', '') input_variables = self.config.get('inputs', []) or self.config.get('input_variables', []) output_variable = self.config.get('output_variable', 'result') if not code: return NodeResult( success=False, error='代码不能为空', ) safe_globals = { '__builtins__': {k: getattr(__builtins__, k) if hasattr(__builtins__, k) else __builtins__[k] for k in self.SAFE_BUILTINS if hasattr(__builtins__, k) or k in __builtins__}, } import json import re import math safe_globals['json'] = json safe_globals['re'] = re safe_globals['math'] = math local_vars = {} for var_config in input_variables: if isinstance(var_config, dict): var_name = var_config.get('variable', '') default_value = var_config.get('default_value', None) if var_name: value = context.get_variable(var_name) if value is not None: local_vars[var_name] = value elif default_value: local_vars[var_name] = context.resolve_template(str(default_value)) else: local_vars[var_name] = None elif isinstance(var_config, str): local_vars[var_config] = context.get_variable(var_config) local_vars['user_input'] = context.user_input local_vars['variables'] = context.variables.copy() exec(code, safe_globals, local_vars) if 'main' in local_vars and callable(local_vars['main']): inputs_dict = { 'user_input': context.user_input, **context.variables, **local_vars, } main_result = local_vars['main'](inputs_dict) if isinstance(main_result, dict): result = main_result.get('result', main_result) else: result = main_result else: result = local_vars.get('result', None) elapsed_time = int((time.time() - start_time) * 1000) return NodeResult( success=True, output=result, output_variables={output_variable: result}, elapsed_time=elapsed_time, ) except Exception as e: logger.exception(f'代码节点执行失败: {e}') elapsed_time = int((time.time() - start_time) * 1000) return NodeResult( success=False, error=str(e), elapsed_time=elapsed_time, ) @classmethod def get_config_schema(cls) -> Dict[str, Any]: """获取配置 Schema""" return { 'type': 'object', 'properties': { 'code': { 'type': 'string', 'title': '代码', 'description': 'Python 代码,结果存储在 result 变量中', 'format': 'code', 'default': '# 在这里编写代码\n# 可用变量: user_input, variables\n# 将结果赋值给 result\n\nresult = user_input.upper()', }, 'input_variables': { 'type': 'array', 'title': '输入变量', 'items': {'type': 'string'}, 'description': '需要传入代码的变量名列表', }, 'output_variable': { 'type': 'string', 'title': '输出变量名', 'default': 'code_result', }, 'timeout': { 'type': 'integer', 'title': '超时时间(秒)', 'default': 30, 'minimum': 1, 'maximum': 300, }, }, 'required': ['code'], }