""" 循环节点 支持两种循环模式: 1. for_each - 遍历数组,对每个元素执行循环体 2. while - 条件循环,满足条件时持续执行 """ import ast import json import logging import operator from typing import Any, Dict, List from ..base import BaseNode, NodeContext, NodeResult from ..registry import NodeRegistry logger = logging.getLogger(__name__) @NodeRegistry.register class LoopNode(BaseNode): """ 循环节点 支持 for_each 和 while 两种循环模式 """ node_type = 'loop' node_name = '循环' node_category = 'logic' node_icon = 'repeat' node_description = '循环执行一组节点,支持遍历数组或条件循环' supports_branches = True inputs = [ { 'name': 'items', 'type': 'array', 'description': '要遍历的数组(for_each 模式)', }, ] outputs = [ { 'name': 'results', 'type': 'array', 'description': '每次循环的结果数组', }, { 'name': 'current_item', 'type': 'any', 'description': '当前循环项(循环体内可用)', }, { 'name': 'current_index', 'type': 'number', 'description': '当前循环索引(从 0 开始)', }, ] # 支持的操作符(用于 while 条件判断) OPERATORS = { 'eq': operator.eq, # 等于 'ne': operator.ne, # 不等于 'gt': operator.gt, # 大于 'gte': operator.ge, # 大于等于 'lt': operator.lt, # 小于 'lte': operator.le, # 小于等于 'is_empty': lambda a, _: not a, # 为空 'is_not_empty': lambda a, _: bool(a), # 不为空 'is_true': lambda a, _: bool(a), # 为真 'is_false': lambda a, _: not bool(a), # 为假 } def execute(self, context: NodeContext) -> NodeResult: """ 执行循环节点 循环节点本身只负责初始化循环状态,实际的循环执行由 WorkflowService 处理 """ try: loop_mode = self.config.get('loop_mode', 'for_each') max_iterations = self.config.get('max_iterations', 100) if loop_mode == 'for_each': return self._init_for_each(context, max_iterations) elif loop_mode == 'while': return self._init_while(context, max_iterations) else: return NodeResult( success=False, error=f'不支持的循环模式: {loop_mode}', ) except Exception as e: logger.exception(f'循环节点执行失败: {e}') return NodeResult( success=False, error=str(e), ) def _init_for_each(self, context: NodeContext, max_iterations: int) -> NodeResult: """初始化 for_each 循环""" items_var = self.config.get('items_variable', '') logger.info(f'[LoopNode] 初始化 for_each 循环,items_variable={items_var}') # 解析数组变量 items = self._resolve_variable(items_var, context) logger.info(f'[LoopNode] 解析后的 items 类型: {type(items).__name__}, 值: {items}') # 如果是字符串,尝试解析为 JSON 或 Python 字面量 if isinstance(items, str): try: items = json.loads(items) logger.info(f'[LoopNode] JSON 解析成功,items={items}') except json.JSONDecodeError: # 尝试使用 ast.literal_eval 解析 Python 字面量(如 str() 输出的列表) try: items = ast.literal_eval(items) logger.info(f'[LoopNode] Python 字面量解析成功,items={items}') except (ValueError, SyntaxError): # 尝试按逗号分割(仅当不是列表/字典格式时) if not (items.strip().startswith('[') or items.strip().startswith('{')): items = [item.strip() for item in items.split(',') if item.strip()] logger.info(f'[LoopNode] 按逗号分割,items={items}') else: logger.error(f'[LoopNode] 无法解析 items 字符串: {items[:200]}...') return NodeResult( success=False, error=f'无法解析 items 变量,格式不正确', ) if not isinstance(items, (list, tuple)): error_msg = f'items 必须是数组,当前类型: {type(items).__name__}, 值: {items}, items_variable={items_var}' logger.error(f'[LoopNode] {error_msg}') return NodeResult( success=False, error=error_msg, ) # 检查数组是否为空 if len(items) == 0: logger.warning(f'[LoopNode] items 数组为空,items_variable={items_var}') # 返回成功但不执行循环体 # 限制最大迭代次数 if len(items) > max_iterations: logger.warning(f'数组长度 {len(items)} 超过最大迭代次数 {max_iterations},将被截断') items = list(items)[:max_iterations] return NodeResult( success=True, output={ 'items': list(items), 'total_count': len(items), }, output_variables={ '_loop_items': list(items), '_loop_index': 0, '_loop_total': len(items), '_loop_mode': 'for_each', '_loop_results': [], }, metadata={ 'is_loop': True, 'loop_mode': 'for_each', 'total_iterations': len(items), 'max_iterations': max_iterations, }, ) def _init_while(self, context: NodeContext, max_iterations: int) -> NodeResult: """初始化 while 循环""" # 检查初始条件 condition_met = self._evaluate_condition(context) return NodeResult( success=True, output={ 'condition_met': condition_met, }, output_variables={ '_loop_index': 0, '_loop_mode': 'while', '_loop_condition_met': condition_met, '_loop_results': [], '_loop_max_iterations': max_iterations, }, metadata={ 'is_loop': True, 'loop_mode': 'while', 'condition_met': condition_met, 'max_iterations': max_iterations, }, ) def check_continue(self, context: NodeContext) -> bool: """ 检查是否继续循环 由 WorkflowService 在每次循环迭代后调用 """ loop_mode = context.get_variable('_loop_mode') current_index = context.get_variable('_loop_index', 0) max_iterations = self.config.get('max_iterations', 100) # 检查最大迭代次数 if current_index >= max_iterations: logger.warning(f'达到最大迭代次数 {max_iterations},停止循环') return False if loop_mode == 'for_each': total = context.get_variable('_loop_total', 0) return current_index < total elif loop_mode == 'while': return self._evaluate_condition(context) return False def get_current_item(self, context: NodeContext) -> Any: """获取当前循环项(for_each 模式)""" items = context.get_variable('_loop_items', []) index = context.get_variable('_loop_index', 0) if 0 <= index < len(items): return items[index] return None def increment_index(self, context: NodeContext) -> int: """增加循环索引""" current_index = context.get_variable('_loop_index', 0) new_index = current_index + 1 context.set_variable('_loop_index', new_index) return new_index def add_result(self, context: NodeContext, result: Any) -> None: """添加循环结果""" results = context.get_variable('_loop_results', []) results.append(result) context.set_variable('_loop_results', results) def _resolve_variable(self, variable: str, context: NodeContext) -> Any: """解析变量引用,支持多层路径访问""" if not isinstance(variable, str): return variable # 如果是空字符串,返回 None if not variable.strip(): return None # 检查是否是 {{...}} 格式的变量引用 if not (variable.startswith('{{') and variable.endswith('}}')): # 不是变量引用,直接返回原始字符串值(可能是硬编码的 JSON 数组或逗号分隔的值) return variable # 使用 resolve_template 解析变量,它支持多层路径访问(如 node.key.subkey) resolved = context.resolve_template(variable) # 如果解析结果与原始变量相同,说明变量不存在或解析失败 if resolved == variable: return None # 如果解析结果是字符串,尝试解析为 JSON if isinstance(resolved, str): try: import json return json.loads(resolved) except (json.JSONDecodeError, TypeError): return resolved return resolved def _evaluate_condition(self, context: NodeContext) -> bool: """评估 while 条件""" conditions = self.config.get('conditions', []) logic = self.config.get('condition_logic', 'and') # and 或 or if not conditions: return False # 无条件默认不继续 results = [] for condition in conditions: result = self._evaluate_single_condition(condition, context) results.append(result) if logic == 'or': return any(results) else: # and return all(results) def _evaluate_single_condition(self, condition: Dict, context: NodeContext) -> bool: """评估单个条件""" variable = condition.get('variable', '') op_name = condition.get('operator', 'eq') value = condition.get('value', '') # 解析变量和值 left_val = self._resolve_variable(variable, context) right_val = self._resolve_variable(value, context) if value else None # 获取操作符函数 op_func = self.OPERATORS.get(op_name, operator.eq) try: # 特殊处理不需要右值的操作符 if op_name in ['is_empty', 'is_not_empty', 'is_true', 'is_false']: return op_func(left_val, None) # 尝试类型转换 if isinstance(left_val, (int, float)) and isinstance(right_val, str): try: if '.' in right_val: right_val = float(right_val) else: right_val = int(right_val) except ValueError: pass return op_func(left_val, right_val) except Exception as e: logger.warning(f'条件评估失败: {e}') return False @classmethod def get_config_schema(cls) -> Dict[str, Any]: """获取配置 Schema""" return { 'type': 'object', 'properties': { 'loop_mode': { 'type': 'string', 'title': '循环模式', 'enum': ['for_each', 'while'], 'enumNames': ['遍历数组 (For Each)', '条件循环 (While)'], 'default': 'for_each', }, 'items_variable': { 'type': 'string', 'title': '数组变量', 'description': '要遍历的数组变量名(for_each 模式)', }, 'item_variable_name': { 'type': 'string', 'title': '循环项变量名', 'description': '当前循环项的变量名,默认为 item', 'default': 'item', }, 'index_variable_name': { 'type': 'string', 'title': '索引变量名', 'description': '当前索引的变量名,默认为 index', 'default': 'index', }, 'conditions': { 'type': 'array', 'title': '循环条件', 'description': 'while 模式的循环条件', 'items': { 'type': 'object', 'properties': { 'variable': { 'type': 'string', 'title': '变量', }, 'operator': { 'type': 'string', 'title': '操作符', 'enum': list(cls.OPERATORS.keys()), 'default': 'eq', }, 'value': { 'type': 'string', 'title': '比较值', }, }, }, }, 'condition_logic': { 'type': 'string', 'title': '条件逻辑', 'enum': ['and', 'or'], 'enumNames': ['全部满足 (AND)', '任一满足 (OR)'], 'default': 'and', }, 'max_iterations': { 'type': 'integer', 'title': '最大迭代次数', 'description': '防止无限循环,默认 100 次', 'default': 100, 'minimum': 1, 'maximum': 10000, }, 'output_variable': { 'type': 'string', 'title': '输出变量名', 'description': '存储所有循环结果的变量名', 'default': 'loop_results', }, }, }