Files
2026-06-08 18:14:59 +08:00

406 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
循环节点
支持两种循环模式:
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',
},
},
}