406 lines
15 KiB
Python
406 lines
15 KiB
Python
"""
|
||
循环节点
|
||
|
||
支持两种循环模式:
|
||
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',
|
||
},
|
||
},
|
||
}
|
||
|
||
|