This commit is contained in:
Typer_Body
2026-05-08 00:56:27 +08:00
parent eb9f38b102
commit 75fdfe6806
51 changed files with 1585 additions and 1643 deletions

View File

@@ -22,15 +22,15 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class CallPipelineNode(WorkflowNode):
"""Call pipeline node - invoke an existing pipeline"""
type_name = "call_pipeline"
category = "action"
icon = "⚙️"
name = "call_pipeline"
description = "call_pipeline"
name_zh = "调用 Pipeline"
name_en = "Call Pipeline"
description_zh = "调用现有的 Pipeline 进行处理"
description_en = "Invoke an existing Pipeline for processing"
type_name = 'call_pipeline'
category = 'action'
icon = '⚙️'
name = 'call_pipeline'
description = 'call_pipeline'
name_zh = '调用 Pipeline'
name_en = 'Call Pipeline'
description_zh = '调用现有的 Pipeline 进行处理'
description_en = 'Invoke an existing Pipeline for processing'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
@@ -66,7 +66,11 @@ class CallPipelineNode(WorkflowNode):
message_event = self._build_message_event(query_text, context)
message_chain = message_event.message_chain
launcher_type = provider_session.LauncherTypes.GROUP if context.message_context and context.message_context.is_group else provider_session.LauncherTypes.PERSON
launcher_type = (
provider_session.LauncherTypes.GROUP
if context.message_context and context.message_context.is_group
else provider_session.LauncherTypes.PERSON
)
launcher_id = context.session_id or context.execution_id
sender_id = (
context.message_context.sender_id
@@ -143,7 +147,9 @@ class CallPipelineNode(WorkflowNode):
return platform_events.FriendMessage(
sender=sender,
message_chain=message_chain,
time=context.message_context.raw_message.get('time') if context.message_context and context.message_context.raw_message else None,
time=context.message_context.raw_message.get('time')
if context.message_context and context.message_context.raw_message
else None,
)

View File

@@ -17,25 +17,25 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class CodeExecutorNode(WorkflowNode):
"""Code executor node - run Python or JavaScript code"""
type_name = "code_executor"
category = "process"
icon = "💻"
name = "code_executor"
description = "code_executor"
name_zh = "代码执行"
name_en = "Code Executor"
description_zh = "执行自定义代码处理数据"
description_en = "Execute custom code to process data"
type_name = 'code_executor'
category = 'process'
icon = '💻'
name = 'code_executor'
description = 'code_executor'
name_zh = '代码执行'
name_en = 'Code Executor'
description_zh = '执行自定义代码处理数据'
description_en = 'Execute custom code to process data'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
code = self.get_config("code", "")
language = self.get_config("language", "python")
code = self.get_config('code', '')
language = self.get_config('language', 'python')
if language == "python":
if language == 'python':
return await self._execute_python(code, inputs, context)
else:
return await self._execute_javascript(code, inputs, context)
@@ -52,22 +52,43 @@ class CodeExecutorNode(WorkflowNode):
restricted_globals = {
'__builtins__': {
'len': len, 'str': str, 'int': int, 'float': float, 'bool': bool,
'list': list, 'dict': dict, 'set': set, 'tuple': tuple,
'range': range, 'enumerate': enumerate, 'zip': zip,
'map': map, 'filter': filter, 'sorted': sorted, 'reversed': reversed,
'sum': sum, 'min': min, 'max': max, 'abs': abs, 'round': round,
'print': print, 'isinstance': isinstance, 'type': type,
'hasattr': hasattr, 'getattr': getattr, 'json': json, 're': re,
'len': len,
'str': str,
'int': int,
'float': float,
'bool': bool,
'list': list,
'dict': dict,
'set': set,
'tuple': tuple,
'range': range,
'enumerate': enumerate,
'zip': zip,
'map': map,
'filter': filter,
'sorted': sorted,
'reversed': reversed,
'sum': sum,
'min': min,
'max': max,
'abs': abs,
'round': round,
'print': print,
'isinstance': isinstance,
'type': type,
'hasattr': hasattr,
'getattr': getattr,
'json': json,
're': re,
}
}
local_vars = {'inputs': inputs, 'output': None}
exec(code, restricted_globals, local_vars)
return {"output": local_vars.get('output'), "console": stdout_capture.getvalue()}
return {'output': local_vars.get('output'), 'console': stdout_capture.getvalue()}
finally:
sys.stdout = old_stdout
async def _execute_javascript(self, code: str, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
return {"output": f"[JS execution not implemented: {code[:50]}...]", "console": ""}
return {'output': f'[JS execution not implemented: {code[:50]}...]', 'console': ''}

View File

@@ -16,82 +16,83 @@ from ..safe_eval import safe_eval_with_vars
class ConditionNode(WorkflowNode):
"""Condition node - branch based on condition"""
type_name = "condition"
category = "control"
icon = "🔀"
name = "condition"
description = "condition"
name_zh = "条件分支"
name_en = "Condition"
description_zh = "根据条件分支工作流"
description_en = "Branch workflow based on a condition"
type_name = 'condition'
category = 'control'
icon = '🔀'
name = 'condition'
description = 'condition'
name_zh = '条件分支'
name_en = 'Condition'
description_zh = '根据条件分支工作流'
description_en = 'Branch workflow based on a condition'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
condition_type = self.get_config("condition_type", "expression")
input_data = inputs.get("input")
condition_type = self.get_config('condition_type', 'expression')
input_data = inputs.get('input')
result = False
if condition_type == "expression":
expression = self.get_config("expression", "false")
if condition_type == 'expression':
expression = self.get_config('expression', 'false')
result = await self._evaluate_expression(expression, input_data, context)
elif condition_type == "comparison":
elif condition_type == 'comparison':
result = await self._evaluate_comparison(input_data, context)
elif condition_type == "contains":
left = self.get_config("left_value", "")
right = self.get_config("right_value", "")
elif condition_type == 'contains':
left = self.get_config('left_value', '')
right = self.get_config('right_value', '')
result = right in left
elif condition_type == "empty":
elif condition_type == 'empty':
result = not bool(input_data)
elif condition_type == "regex":
elif condition_type == 'regex':
import re
left = self.get_config("left_value", "")
pattern = self.get_config("right_value", "")
left = self.get_config('left_value', '')
pattern = self.get_config('right_value', '')
result = bool(re.match(pattern, str(left)))
if result:
return {"true": input_data, "false": None}
return {'true': input_data, 'false': None}
else:
return {"true": None, "false": input_data}
return {'true': None, 'false': input_data}
async def _evaluate_expression(self, expression: str, data: Any, context: ExecutionContext) -> bool:
try:
local_vars = {"input": data, "data": data, "variables": context.variables}
local_vars = {'input': data, 'data': data, 'variables': context.variables}
return bool(safe_eval_with_vars(expression, local_vars))
except Exception:
return False
async def _evaluate_comparison(self, data: Any, context: ExecutionContext) -> bool:
left = self.get_config("left_value", "")
right = self.get_config("right_value", "")
operator = self.get_config("operator", "==")
left = self.get_config('left_value', '')
right = self.get_config('right_value', '')
operator = self.get_config('operator', '==')
try:
left_num = float(left)
right_num = float(right)
if operator == "==":
if operator == '==':
return left_num == right_num
elif operator == "!=":
elif operator == '!=':
return left_num != right_num
elif operator == ">":
elif operator == '>':
return left_num > right_num
elif operator == "<":
elif operator == '<':
return left_num < right_num
elif operator == ">=":
elif operator == '>=':
return left_num >= right_num
elif operator == "<=":
elif operator == '<=':
return left_num <= right_num
except ValueError:
if operator == "==":
if operator == '==':
return left == right
elif operator == "!=":
elif operator == '!=':
return left != right
elif operator in (">", "<", ">=", "<="):
elif operator in ('>', '<', '>=', '<='):
return False
return False

View File

@@ -15,35 +15,35 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class CozeBotNode(WorkflowNode):
"""Coze bot node - call Coze API bot"""
type_name = "coze_bot"
category = "integration"
icon = "MessageSquare"
name = "coze_bot"
description = "coze_bot"
name_zh = "Coze Bot"
name_en = "Coze Bot"
description_zh = "调用扣子 Bot"
description_en = "Call a Coze Bot"
type_name = 'coze_bot'
category = 'integration'
icon = 'MessageSquare'
name = 'coze_bot'
description = 'coze_bot'
name_zh = 'Coze Bot'
name_en = 'Coze Bot'
description_zh = '调用扣子 Bot'
description_en = 'Call a Coze Bot'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
api_key = self.get_config("api_key", "")
bot_id = self.get_config("bot_id", "")
api_base = self.get_config("api_base", "https://api.coze.cn")
query = inputs.get("query", "")
conversation_id = inputs.get("conversation_id")
api_key = self.get_config('api_key', '')
bot_id = self.get_config('bot_id', '')
api_base = self.get_config('api_base', 'https://api.coze.cn')
query = inputs.get('query', '')
conversation_id = inputs.get('conversation_id')
return {
"answer": "",
"conversation_id": conversation_id,
"success": False,
"_debug": {
"api_key": api_key[:8] + "..." if api_key else "",
"bot_id": bot_id,
"api_base": api_base,
"query": query,
'answer': '',
'conversation_id': conversation_id,
'success': False,
'_debug': {
'api_key': api_key[:8] + '...' if api_key else '',
'bot_id': bot_id,
'api_base': api_base,
'query': query,
},
}

View File

@@ -15,15 +15,15 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class CronTriggerNode(WorkflowNode):
"""Cron trigger node - triggers workflow on schedule"""
type_name = "cron_trigger"
category = "trigger"
icon = ""
name = "cron_trigger"
description = "cron_trigger"
name_zh = "定时触发"
name_en = "Scheduled Trigger"
description_zh = "按定时计划触发工作流"
description_en = "Trigger workflow on a scheduled time"
type_name = 'cron_trigger'
category = 'trigger'
icon = ''
name = 'cron_trigger'
description = 'cron_trigger'
name_zh = '定时触发'
name_en = 'Scheduled Trigger'
description_zh = '按定时计划触发工作流'
description_en = 'Trigger workflow on a scheduled time'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
@@ -33,7 +33,7 @@ class CronTriggerNode(WorkflowNode):
from datetime import datetime
return {
"timestamp": datetime.now().isoformat(),
"schedule": self.get_config("cron", ""),
"context": context.trigger_data,
'timestamp': datetime.now().isoformat(),
'schedule': self.get_config('cron', ''),
'context': context.trigger_data,
}

View File

@@ -16,52 +16,52 @@ from ..safe_eval import safe_eval_with_vars
class DataTransformNode(WorkflowNode):
"""Data transform node - transform data using templates or JSONPath"""
type_name = "data_transform"
category = "process"
icon = "🔄"
name = "data_transform"
description = "data_transform"
name_zh = "数据转换"
name_en = "Data Transform"
description_zh = "使用模板或 JSONPath 转换数据"
description_en = "Transform data using templates or JSONPath"
type_name = 'data_transform'
category = 'process'
icon = '🔄'
name = 'data_transform'
description = 'data_transform'
name_zh = '数据转换'
name_en = 'Data Transform'
description_zh = '使用模板或 JSONPath 转换数据'
description_en = 'Transform data using templates or JSONPath'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
data = inputs.get("data")
transform_type = self.get_config("transform_type", "template")
data = inputs.get('data')
transform_type = self.get_config('transform_type', 'template')
if transform_type == "template":
template = self.get_config("template", "")
if transform_type == 'template':
template = self.get_config('template', '')
result = self._apply_template(template, data, context)
elif transform_type == "jsonpath":
expression = self.get_config("expression", "$")
elif transform_type == 'jsonpath':
expression = self.get_config('expression', '$')
result = self._apply_jsonpath(expression, data)
elif transform_type == "expression":
expression = self.get_config("expression", "")
elif transform_type == 'expression':
expression = self.get_config('expression', '')
result = self._evaluate_expression(expression, data, context)
else:
result = data
return {"result": result}
return {'result': result}
def _apply_template(self, template: str, data: Any, context: ExecutionContext) -> str:
result = template
if isinstance(data, dict):
for key, value in data.items():
result = result.replace(f"{{{{data.{key}}}}}", str(value))
result = result.replace(f'{{{{data.{key}}}}}', str(value))
for key, value in context.variables.items():
result = result.replace(f"{{{{variables.{key}}}}}", str(value))
result = result.replace(f'{{{{variables.{key}}}}}', str(value))
return result
def _apply_jsonpath(self, expression: str, data: Any) -> Any:
if expression == "$":
if expression == '$':
return data
if expression.startswith("$."):
parts = expression[2:].split(".")
if expression.startswith('$.'):
parts = expression[2:].split('.')
result = data
for part in parts:
if isinstance(result, dict):
@@ -74,7 +74,7 @@ class DataTransformNode(WorkflowNode):
return data
def _evaluate_expression(self, expression: str, data: Any, context: ExecutionContext) -> Any:
local_vars = {"data": data, "variables": context.variables}
local_vars = {'data': data, 'variables': context.variables}
try:
return safe_eval_with_vars(expression, local_vars)
except Exception:

View File

@@ -15,37 +15,37 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class DatabaseQueryNode(WorkflowNode):
"""Database query node - execute database queries"""
type_name = "database_query"
category = "integration"
icon = "Database"
name = "database_query"
description = "database_query"
name_zh = "数据库查询"
name_en = "Database Query"
description_zh = "执行数据库查询"
description_en = "Execute database queries"
type_name = 'database_query'
category = 'integration'
icon = 'Database'
name = 'database_query'
description = 'database_query'
name_zh = '数据库查询'
name_en = 'Database Query'
description_zh = '执行数据库查询'
description_en = 'Execute database queries'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
connection_type = self.get_config("connection_type", "postgresql")
query = self.get_config("query", "")
query_type = self.get_config("query_type", "select")
timeout = self.get_config("timeout", 30)
connection_type = self.get_config('connection_type', 'postgresql')
query = self.get_config('query', '')
query_type = self.get_config('query_type', 'select')
timeout = self.get_config('timeout', 30)
parameters = inputs.get("parameters", {})
parameters = inputs.get('parameters', {})
return {
"results": [],
"row_count": 0,
"success": False,
"_debug": {
"connection_type": connection_type,
"query": query,
"query_type": query_type,
"timeout": timeout,
"parameters": parameters,
'results': [],
'row_count': 0,
'success': False,
'_debug': {
'connection_type': connection_type,
'query': query,
'query_type': query_type,
'timeout': timeout,
'parameters': parameters,
},
}

View File

@@ -15,33 +15,33 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class DifyKnowledgeQueryNode(WorkflowNode):
"""Dify knowledge base query node - query Dify knowledge base"""
type_name = "dify_knowledge_query"
category = "integration"
icon = "BookOpen"
name = "dify_knowledge_query"
description = "dify_knowledge_query"
name_zh = "Dify 知识库查询"
name_en = "Dify Knowledge Query"
description_zh = "查询 Dify 知识库"
description_en = "Query Dify knowledge base"
type_name = 'dify_knowledge_query'
category = 'integration'
icon = 'BookOpen'
name = 'dify_knowledge_query'
description = 'dify_knowledge_query'
name_zh = 'Dify 知识库查询'
name_en = 'Dify Knowledge Query'
description_zh = '查询 Dify 知识库'
description_en = 'Query Dify knowledge base'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
base_url = self.get_config("base_url", "https://api.dify.ai/v1")
api_key = self.get_config("api_key", "")
dataset_id = self.get_config("dataset_id", "")
query = inputs.get("query", "")
base_url = self.get_config('base_url', 'https://api.dify.ai/v1')
api_key = self.get_config('api_key', '')
dataset_id = self.get_config('dataset_id', '')
query = inputs.get('query', '')
return {
"results": [],
"success": False,
"_debug": {
"base_url": base_url,
"api_key": api_key[:8] + "..." if api_key else "",
"dataset_id": dataset_id,
"query": query,
'results': [],
'success': False,
'_debug': {
'base_url': base_url,
'api_key': api_key[:8] + '...' if api_key else '',
'dataset_id': dataset_id,
'query': query,
},
}

View File

@@ -15,35 +15,35 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class DifyWorkflowNode(WorkflowNode):
"""Dify workflow node - call Dify service API"""
type_name = "dify_workflow"
category = "integration"
icon = "Bot"
name = "dify_workflow"
description = "dify_workflow"
name_zh = "Dify 工作流"
name_en = "Dify Workflow"
description_zh = "调用 Dify 平台工作流"
description_en = "Call a Dify platform workflow"
type_name = 'dify_workflow'
category = 'integration'
icon = 'Bot'
name = 'dify_workflow'
description = 'dify_workflow'
name_zh = 'Dify 工作流'
name_en = 'Dify Workflow'
description_zh = '调用 Dify 平台工作流'
description_en = 'Call a Dify platform workflow'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
base_url = self.get_config("base_url", "https://api.dify.ai/v1")
api_key = self.get_config("api_key", "")
app_type = self.get_config("app_type", "chat")
query = inputs.get("query", "")
conversation_id = inputs.get("conversation_id")
base_url = self.get_config('base_url', 'https://api.dify.ai/v1')
api_key = self.get_config('api_key', '')
app_type = self.get_config('app_type', 'chat')
query = inputs.get('query', '')
conversation_id = inputs.get('conversation_id')
return {
"answer": "",
"conversation_id": conversation_id,
"success": False,
"_debug": {
"base_url": base_url,
"api_key": api_key[:8] + "..." if api_key else "",
"app_type": app_type,
"query": query,
'answer': '',
'conversation_id': conversation_id,
'success': False,
'_debug': {
'base_url': base_url,
'api_key': api_key[:8] + '...' if api_key else '',
'app_type': app_type,
'query': query,
},
}

View File

@@ -15,31 +15,32 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class EndNode(WorkflowNode):
"""End node - marks the end of workflow execution"""
type_name = "end"
category = "action"
icon = "🏁"
name = "end"
description = "end"
name_zh = "结束"
name_en = "End"
description_zh = "结束工作流执行"
description_en = "End the workflow execution"
type_name = 'end'
category = 'action'
icon = '🏁'
name = 'end'
description = 'end'
name_zh = '结束'
name_en = 'End'
description_zh = '结束工作流执行'
description_en = 'End the workflow execution'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
result = inputs.get("result")
output_format = self.get_config("output_format", "passthrough")
result = inputs.get('result')
output_format = self.get_config('output_format', 'passthrough')
if output_format == "text":
return {"output": str(result)}
elif output_format == "json":
if output_format == 'text':
return {'output': str(result)}
elif output_format == 'json':
import json
try:
return {"output": json.dumps(result, ensure_ascii=False)}
return {'output': json.dumps(result, ensure_ascii=False)}
except Exception:
return {"output": str(result)}
return {'output': str(result)}
else:
return {"output": result}
return {'output': result}

View File

@@ -15,15 +15,15 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class EventTriggerNode(WorkflowNode):
"""Event trigger node - triggers workflow on system events"""
type_name = "event_trigger"
category = "trigger"
icon = "📡"
name = "event_trigger"
description = "event_trigger"
name_zh = "事件触发"
name_en = "Event Trigger"
description_zh = "当系统事件发生时触发工作流"
description_en = "Trigger workflow when a system event occurs"
type_name = 'event_trigger'
category = 'trigger'
icon = '📡'
name = 'event_trigger'
description = 'event_trigger'
name_zh = '事件触发'
name_en = 'Event Trigger'
description_zh = '当系统事件发生时触发工作流'
description_en = 'Trigger workflow when a system event occurs'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
@@ -35,7 +35,7 @@ class EventTriggerNode(WorkflowNode):
trigger_data = context.trigger_data
return {
"event_type": trigger_data.get("event_type", ""),
"event_data": trigger_data.get("event_data", {}),
"timestamp": trigger_data.get("timestamp", datetime.now().isoformat()),
'event_type': trigger_data.get('event_type', ''),
'event_data': trigger_data.get('event_data', {}),
'timestamp': trigger_data.get('timestamp', datetime.now().isoformat()),
}

View File

@@ -15,15 +15,15 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class HTTPRequestNode(WorkflowNode):
"""HTTP request node - make HTTP API calls"""
type_name = "http_request"
category = "process"
icon = "🌐"
name = "http_request"
description = "http_request"
name_zh = "HTTP 请求"
name_en = "HTTP Request"
description_zh = "向外部 API 发送 HTTP 请求"
description_en = "Make HTTP requests to external APIs"
type_name = 'http_request'
category = 'process'
icon = '🌐'
name = 'http_request'
description = 'http_request'
name_zh = 'HTTP 请求'
name_en = 'HTTP Request'
description_zh = '向外部 API 发送 HTTP 请求'
description_en = 'Make HTTP requests to external APIs'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
@@ -32,39 +32,44 @@ class HTTPRequestNode(WorkflowNode):
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
import aiohttp
url = self.get_config("url", "")
method = self.get_config("method", "GET")
timeout = self.get_config("timeout", 30)
content_type = self.get_config("content_type", "application/json")
url = self.get_config('url', '')
method = self.get_config('method', 'GET')
timeout = self.get_config('timeout', 30)
content_type = self.get_config('content_type', 'application/json')
headers = inputs.get("headers", {})
headers["Content-Type"] = content_type
headers = inputs.get('headers', {})
headers['Content-Type'] = content_type
auth_type = self.get_config("auth_type", "none")
auth_config = self.get_config("auth_config", {})
auth_type = self.get_config('auth_type', 'none')
auth_config = self.get_config('auth_config', {})
if auth_type == "bearer":
headers["Authorization"] = f"Bearer {auth_config.get('token', '')}"
elif auth_type == "api_key":
header_name = auth_config.get("header", "X-API-Key")
headers[header_name] = auth_config.get("key", "")
if auth_type == 'bearer':
headers['Authorization'] = f'Bearer {auth_config.get("token", "")}'
elif auth_type == 'api_key':
header_name = auth_config.get('header', 'X-API-Key')
headers[header_name] = auth_config.get('key', '')
body = inputs.get("body")
body = inputs.get('body')
try:
async with aiohttp.ClientSession() as session:
async with session.request(
method=method, url=url,
json=body if content_type == "application/json" else None,
data=body if content_type != "application/json" else None,
method=method,
url=url,
json=body if content_type == 'application/json' else None,
data=body if content_type != 'application/json' else None,
headers=headers,
timeout=aiohttp.ClientTimeout(total=timeout)
timeout=aiohttp.ClientTimeout(total=timeout),
) as response:
try:
response_data = await response.json()
except Exception:
response_data = await response.text()
return {"response": response_data, "status_code": response.status, "headers": dict(response.headers)}
return {
'response': response_data,
'status_code': response.status,
'headers': dict(response.headers),
}
except Exception as e:
return {"response": None, "status_code": 0, "headers": {}, "error": str(e)}
return {'response': None, 'status_code': 0, 'headers': {}, 'error': str(e)}

View File

@@ -12,49 +12,52 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class IteratorNode(WorkflowNode):
"""Iterator node - iterate over array items one by one"""
type_name = "iterator"
category = "control"
icon = "🔄"
name = "iterator"
name_zh = "迭代器"
name_en = "Iterator"
description = "iterator"
description_zh = "逐个遍历数组元素"
description_en = "Iterate over array elements one by one"
type_name = 'iterator'
category = 'control'
icon = '🔄'
name = 'iterator'
name_zh = '迭代器'
name_en = 'Iterator'
description = 'iterator'
description_zh = '逐个遍历数组元素'
description_en = 'Iterate over array elements one by one'
inputs: ClassVar[list[NodePort]] = [
NodePort(name="items", type="array", description="Array to iterate over", required=True),
NodePort(name='items', type='array', description='Array to iterate over', required=True),
]
outputs: ClassVar[list[NodePort]] = [
NodePort(name="item", type="any", description="Current item"),
NodePort(name="index", type="number", description="Current index"),
NodePort(name="is_first", type="boolean", description="Whether this is the first item"),
NodePort(name="is_last", type="boolean", description="Whether this is the last item"),
NodePort(name="results", type="array", description="All iteration results"),
NodePort(name="completed", type="boolean", description="Whether iteration completed"),
NodePort(name='item', type='any', description='Current item'),
NodePort(name='index', type='number', description='Current index'),
NodePort(name='is_first', type='boolean', description='Whether this is the first item'),
NodePort(name='is_last', type='boolean', description='Whether this is the last item'),
NodePort(name='results', type='array', description='All iteration results'),
NodePort(name='completed', type='boolean', description='Whether iteration completed'),
]
config_schema: ClassVar[list[NodeConfig]] = [
NodeConfig(
name="max_iterations", type="integer", required=False, default=1000,
description="Maximum iterations (safety limit)",
label={"en_US": "Max Iterations", "zh_Hans": "最大迭代次数"},
name='max_iterations',
type='integer',
required=False,
default=1000,
description='Maximum iterations (safety limit)',
label={'en_US': 'Max Iterations', 'zh_Hans': '最大迭代次数'},
),
]
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
items = inputs.get("items", [])
items = inputs.get('items', [])
if not isinstance(items, list):
items = [items] if items else []
max_iterations = self.get_config("max_iterations", 1000)
max_iterations = self.get_config('max_iterations', 1000)
items = items[:max_iterations]
return {
"item": items[0] if items else None,
"index": 0,
"is_first": True,
"is_last": len(items) <= 1,
"results": [],
"completed": len(items) == 0,
"_items": items,
'item': items[0] if items else None,
'index': 0,
'is_first': True,
'is_last': len(items) <= 1,
'results': [],
'completed': len(items) == 0,
'_items': items,
}

View File

@@ -15,20 +15,20 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class KnowledgeRetrievalNode(WorkflowNode):
"""Knowledge retrieval node - search in knowledge base"""
type_name = "knowledge_retrieval"
category = "process"
icon = "📚"
name = "knowledge_retrieval"
description = "knowledge_retrieval"
name_zh = "知识库检索"
name_en = "Knowledge Retrieval"
description_zh = "从知识库中检索相关信息"
description_en = "Retrieve relevant information from knowledge bases"
type_name = 'knowledge_retrieval'
category = 'process'
icon = '📚'
name = 'knowledge_retrieval'
description = 'knowledge_retrieval'
name_zh = '知识库检索'
name_en = 'Knowledge Retrieval'
description_zh = '从知识库中检索相关信息'
description_en = 'Retrieve relevant information from knowledge bases'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
query = inputs.get("query", "")
return {"documents": [], "citations": [], "context": f"[Knowledge base search for: {query}]"}
query = inputs.get('query', '')
return {'documents': [], 'citations': [], 'context': f'[Knowledge base search for: {query}]'}

View File

@@ -15,33 +15,33 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class LangflowFlowNode(WorkflowNode):
"""Langflow flow node - call Langflow API"""
type_name = "langflow_flow"
category = "integration"
icon = "GitBranch"
name = "langflow_flow"
description = "langflow_flow"
name_zh = "Langflow 流程"
name_en = "Langflow Flow"
description_zh = "调用 Langflow 流程"
description_en = "Call a Langflow flow"
type_name = 'langflow_flow'
category = 'integration'
icon = 'GitBranch'
name = 'langflow_flow'
description = 'langflow_flow'
name_zh = 'Langflow 流程'
name_en = 'Langflow Flow'
description_zh = '调用 Langflow 流程'
description_en = 'Call a Langflow flow'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
base_url = self.get_config("base_url", "http://localhost:7860")
api_key = self.get_config("api_key", "")
flow_id = self.get_config("flow_id", "")
input_value = inputs.get("input_value", "")
base_url = self.get_config('base_url', 'http://localhost:7860')
api_key = self.get_config('api_key', '')
flow_id = self.get_config('flow_id', '')
input_value = inputs.get('input_value', '')
return {
"result": None,
"success": False,
"_debug": {
"base_url": base_url,
"api_key": api_key[:8] + "..." if api_key else "",
"flow_id": flow_id,
"input_value": input_value,
'result': None,
'success': False,
'_debug': {
'base_url': base_url,
'api_key': api_key[:8] + '...' if api_key else '',
'flow_id': flow_id,
'input_value': input_value,
},
}

View File

@@ -18,108 +18,120 @@ logger = logging.getLogger(__name__)
class LLMCallNode(WorkflowNode):
"""LLM call node - invoke large language model"""
type_name = "llm_call"
category = "process"
icon = "🤖"
name = "llm_call"
name_zh = "LLM 调用"
name_en = "LLM Call"
description = "llm_call"
description_zh = "调用大语言模型生成响应"
description_en = "Call a large language model to generate responses"
type_name = 'llm_call'
category = 'process'
icon = '🤖'
name = 'llm_call'
name_zh = 'LLM 调用'
name_en = 'LLM Call'
description = 'llm_call'
description_zh = '调用大语言模型生成响应'
description_en = 'Call a large language model to generate responses'
inputs: ClassVar[list[NodePort]] = [
NodePort(name="input", type="string", description="Input text to send to the model", required=False),
NodePort(name="context", type="object", description="Additional context data", required=False),
NodePort(name='input', type='string', description='Input text to send to the model', required=False),
NodePort(name='context', type='object', description='Additional context data', required=False),
]
outputs: ClassVar[list[NodePort]] = [
NodePort(name="response", type="string", description="Model response text"),
NodePort(name="usage", type="object", description="Token usage information"),
NodePort(name='response', type='string', description='Model response text'),
NodePort(name='usage', type='object', description='Token usage information'),
]
config_schema: ClassVar[list[NodeConfig]] = [
NodeConfig(
name="model", type="llm-model-selector", required=True,
description="Select the LLM model to use",
label={"en_US": "Model", "zh_Hans": "模型"},
name='model',
type='llm-model-selector',
required=True,
description='Select the LLM model to use',
label={'en_US': 'Model', 'zh_Hans': '模型'},
),
NodeConfig(
name="system_prompt", type="textarea", required=False, default="",
description="System prompt to set model behavior",
label={"en_US": "System Prompt", "zh_Hans": "系统提示词"},
name='system_prompt',
type='textarea',
required=False,
default='',
description='System prompt to set model behavior',
label={'en_US': 'System Prompt', 'zh_Hans': '系统提示词'},
),
NodeConfig(
name="user_prompt_template", type="textarea", required=True, default="{{input}}",
description="User prompt template with variable placeholders",
label={"en_US": "User Prompt Template", "zh_Hans": "用户提示词模板"},
name='user_prompt_template',
type='textarea',
required=True,
default='{{input}}',
description='User prompt template with variable placeholders',
label={'en_US': 'User Prompt Template', 'zh_Hans': '用户提示词模板'},
),
NodeConfig(
name="temperature", type="number", required=False, default=0.7,
description="Controls randomness (0.0-2.0)",
label={"en_US": "Temperature", "zh_Hans": "温度"},
min_value=0.0, max_value=2.0,
name='temperature',
type='number',
required=False,
default=0.7,
description='Controls randomness (0.0-2.0)',
label={'en_US': 'Temperature', 'zh_Hans': '温度'},
min_value=0.0,
max_value=2.0,
),
NodeConfig(
name="max_tokens", type="integer", required=False, default=0,
description="Max tokens to generate (0 = model default)",
label={"en_US": "Max Tokens", "zh_Hans": "最大令牌数"},
name='max_tokens',
type='integer',
required=False,
default=0,
description='Max tokens to generate (0 = model default)',
label={'en_US': 'Max Tokens', 'zh_Hans': '最大令牌数'},
),
]
def _resolve_template(self, template: str, inputs: dict[str, Any], context: ExecutionContext) -> str:
"""Resolve {{variable}} placeholders in a template string."""
def replacer(match: re.Match) -> str:
expr = match.group(1).strip()
# Try inputs first
if expr in inputs:
return str(inputs[expr])
# Try context variables
if expr.startswith("variables."):
var_name = expr[len("variables."):]
return str(context.variables.get(var_name, ""))
if expr.startswith('variables.'):
var_name = expr[len('variables.') :]
return str(context.variables.get(var_name, ''))
# Try message context
if expr.startswith("message.") and context.message_context:
attr = expr[len("message."):]
return str(getattr(context.message_context, attr, ""))
if expr.startswith('message.') and context.message_context:
attr = expr[len('message.') :]
return str(getattr(context.message_context, attr, ''))
return match.group(0) # leave unresolved
return re.sub(r"\{\{([^}]+)\}\}", replacer, template)
return re.sub(r'\{\{([^}]+)\}\}', replacer, template)
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
model_uuid = self.get_config("model", "")
model_uuid = self.get_config('model', '')
if not model_uuid:
raise ValueError("No model configured for LLM call node")
raise ValueError('No model configured for LLM call node')
if not self.ap:
raise RuntimeError("Application instance not available — cannot call LLM")
raise RuntimeError('Application instance not available — cannot call LLM')
# Resolve prompts
system_prompt = self._resolve_template(
self.get_config("system_prompt", ""), inputs, context
)
user_prompt = self._resolve_template(
self.get_config("user_prompt_template", "{{input}}"), inputs, context
)
system_prompt = self._resolve_template(self.get_config('system_prompt', ''), inputs, context)
user_prompt = self._resolve_template(self.get_config('user_prompt_template', '{{input}}'), inputs, context)
# Build messages
messages: list[provider_message.Message] = []
if system_prompt:
messages.append(provider_message.Message(role="system", content=system_prompt))
messages.append(provider_message.Message(role="user", content=user_prompt))
messages.append(provider_message.Message(role='system', content=system_prompt))
messages.append(provider_message.Message(role='user', content=user_prompt))
# Get model
runtime_model = await self.ap.model_mgr.get_model_by_uuid(model_uuid)
# Build extra args from config
extra_args: dict[str, Any] = {}
temperature = self.get_config("temperature")
temperature = self.get_config('temperature')
if temperature is not None:
extra_args["temperature"] = float(temperature)
max_tokens = self.get_config("max_tokens", 0)
extra_args['temperature'] = float(temperature)
max_tokens = self.get_config('max_tokens', 0)
if max_tokens and int(max_tokens) > 0:
extra_args["max_tokens"] = int(max_tokens)
extra_args['max_tokens'] = int(max_tokens)
# Invoke LLM
logger.info(f"LLM call node {self.node_id}: invoking model {model_uuid}")
logger.info(f'LLM call node {self.node_id}: invoking model {model_uuid}')
result_message = await runtime_model.provider.invoke_llm(
query=None,
model=runtime_model,
@@ -129,7 +141,7 @@ class LLMCallNode(WorkflowNode):
)
# Extract response text
response_text = ""
response_text = ''
if isinstance(result_message.content, str):
response_text = result_message.content
elif isinstance(result_message.content, list):
@@ -141,23 +153,23 @@ class LLMCallNode(WorkflowNode):
response_text += elem
# Extract usage info if available
usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
usage = {'prompt_tokens': 0, 'completion_tokens': 0, 'total_tokens': 0}
if hasattr(result_message, 'usage') and result_message.usage:
u = result_message.usage
usage = {
"prompt_tokens": getattr(u, 'prompt_tokens', 0) or 0,
"completion_tokens": getattr(u, 'completion_tokens', 0) or 0,
"total_tokens": getattr(u, 'total_tokens', 0) or 0,
'prompt_tokens': getattr(u, 'prompt_tokens', 0) or 0,
'completion_tokens': getattr(u, 'completion_tokens', 0) or 0,
'total_tokens': getattr(u, 'total_tokens', 0) or 0,
}
elif hasattr(result_message, 'token_usage') and result_message.token_usage:
u = result_message.token_usage
usage = {
"prompt_tokens": getattr(u, 'prompt_tokens', 0) or 0,
"completion_tokens": getattr(u, 'completion_tokens', 0) or 0,
"total_tokens": getattr(u, 'total_tokens', 0) or 0,
'prompt_tokens': getattr(u, 'prompt_tokens', 0) or 0,
'completion_tokens': getattr(u, 'completion_tokens', 0) or 0,
'total_tokens': getattr(u, 'total_tokens', 0) or 0,
}
return {
"response": response_text,
"usage": usage,
'response': response_text,
'usage': usage,
}

View File

@@ -12,51 +12,57 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class LoopNode(WorkflowNode):
"""Loop node - iterate over items"""
type_name = "loop"
category = "control"
icon = "🔁"
name = "loop"
name_zh = "循环"
name_en = "Loop"
description = "loop"
description_zh = "遍历项目或重复直到满足条件"
description_en = "Iterate over items or repeat until condition"
type_name = 'loop'
category = 'control'
icon = '🔁'
name = 'loop'
name_zh = '循环'
name_en = 'Loop'
description = 'loop'
description_zh = '遍历项目或重复直到满足条件'
description_en = 'Iterate over items or repeat until condition'
inputs: ClassVar[list[NodePort]] = [
NodePort(name="items", type="array", description="Items to iterate over", required=False),
NodePort(name='items', type='array', description='Items to iterate over', required=False),
]
outputs: ClassVar[list[NodePort]] = [
NodePort(name="item", type="any", description="Current item in iteration"),
NodePort(name="index", type="number", description="Current iteration index"),
NodePort(name="results", type="array", description="All iteration results"),
NodePort(name="completed", type="boolean", description="Whether loop completed"),
NodePort(name='item', type='any', description='Current item in iteration'),
NodePort(name='index', type='number', description='Current iteration index'),
NodePort(name='results', type='array', description='All iteration results'),
NodePort(name='completed', type='boolean', description='Whether loop completed'),
]
config_schema: ClassVar[list[NodeConfig]] = [
NodeConfig(
name="loop_type", type="select", required=True, default="foreach",
description="Type of loop",
label={"en_US": "Loop Type", "zh_Hans": "循环类型"},
options=["foreach", "while", "count"],
name='loop_type',
type='select',
required=True,
default='foreach',
description='Type of loop',
label={'en_US': 'Loop Type', 'zh_Hans': '循环类型'},
options=['foreach', 'while', 'count'],
),
NodeConfig(
name="max_iterations", type="integer", required=False, default=100,
description="Maximum iterations (safety limit)",
label={"en_US": "Max Iterations", "zh_Hans": "最大迭代次数"},
name='max_iterations',
type='integer',
required=False,
default=100,
description='Maximum iterations (safety limit)',
label={'en_US': 'Max Iterations', 'zh_Hans': '最大迭代次数'},
),
]
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
items = inputs.get("items", [])
items = inputs.get('items', [])
if not isinstance(items, list):
items = [items] if items else []
max_iterations = self.get_config("max_iterations", 100)
max_iterations = self.get_config('max_iterations', 100)
items = items[:max_iterations]
return {
"item": items[0] if items else None,
"index": 0,
"results": [],
"completed": len(items) == 0,
"_items": items,
'item': items[0] if items else None,
'index': 0,
'results': [],
'completed': len(items) == 0,
'_items': items,
}

View File

@@ -20,21 +20,21 @@ class MCPToolNode(WorkflowNode):
"""MCP tool node - invoke MCP (Model Context Protocol) tools"""
# Node type for registration
type_name = "mcp_tool"
type_name = 'mcp_tool'
# Category and icon - these are not i18n
category = "integration"
icon = "Wrench"
category = 'integration'
icon = 'Wrench'
# Name and description - i18n handled on frontend side
# Frontend will use node type key to look up translation
name = "mcp_tool"
description = "mcp_tool"
name_zh = "MCP 工具"
name_en = "MCP Tool"
description_zh = "调用 MCP 工具"
description_en = "Invoke an MCP (Model Context Protocol) tool"
name = 'mcp_tool'
description = 'mcp_tool'
name_zh = 'MCP 工具'
name_en = 'MCP Tool'
description_zh = '调用 MCP 工具'
description_en = 'Invoke an MCP (Model Context Protocol) tool'
# Inputs/outputs/config - loaded from YAML at runtime
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
@@ -42,29 +42,29 @@ class MCPToolNode(WorkflowNode):
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
"""Execute the MCP tool node
Args:
inputs: Input data from connected nodes
context: Execution context with workflow state
Returns:
Dictionary of output values
"""
server_name = self.get_config("server_name", "")
tool_name = self.get_config("tool_name", "")
arguments_template = self.get_config("arguments_template", "")
timeout = self.get_config("timeout", 30)
server_name = self.get_config('server_name', '')
tool_name = self.get_config('tool_name', '')
arguments_template = self.get_config('arguments_template', '')
timeout = self.get_config('timeout', 30)
arguments = inputs.get("arguments", arguments_template)
arguments = inputs.get('arguments', arguments_template)
return {
"result": None,
"success": False,
"error": f"MCP tool '{server_name}/{tool_name}' not implemented yet",
"_debug": {
"server_name": server_name,
"tool_name": tool_name,
"arguments": arguments,
"timeout": timeout,
'result': None,
'success': False,
'error': f"MCP tool '{server_name}/{tool_name}' not implemented yet",
'_debug': {
'server_name': server_name,
'tool_name': tool_name,
'arguments': arguments,
'timeout': timeout,
},
}

View File

@@ -13,35 +13,31 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class MemoryHelper:
"""Helper class wrapping context.memory dict with get/set/delete/list_all/append operations"""
def __init__(self, memory_dict: dict[str, Any]):
self._data = memory_dict
def get(self, key: str, scope: str = "execution", default: Any = None) -> Any:
def get(self, key: str, scope: str = 'execution', default: Any = None) -> Any:
"""Get a value from memory by key"""
scoped_key = f"{scope}:{key}" if scope else key
scoped_key = f'{scope}:{key}' if scope else key
return self._data.get(scoped_key, default)
def set(self, key: str, value: Any, scope: str = "execution", ttl: int = 0) -> None:
def set(self, key: str, value: Any, scope: str = 'execution', ttl: int = 0) -> None:
"""Set a value in memory"""
scoped_key = f"{scope}:{key}" if scope else key
scoped_key = f'{scope}:{key}' if scope else key
self._data[scoped_key] = value
def delete(self, key: str, scope: str = "execution") -> None:
def delete(self, key: str, scope: str = 'execution') -> None:
"""Delete a value from memory"""
scoped_key = f"{scope}:{key}" if scope else key
scoped_key = f'{scope}:{key}' if scope else key
self._data.pop(scoped_key, None)
def list_all(self, scope: str = "execution") -> dict[str, Any]:
def list_all(self, scope: str = 'execution') -> dict[str, Any]:
"""List all values in the given scope"""
prefix = f"{scope}:"
return {
k[len(prefix):]: v
for k, v in self._data.items()
if k.startswith(prefix)
}
def append(self, key: str, value: Any, scope: str = "execution", ttl: int = 0) -> list:
prefix = f'{scope}:'
return {k[len(prefix) :]: v for k, v in self._data.items() if k.startswith(prefix)}
def append(self, key: str, value: Any, scope: str = 'execution', ttl: int = 0) -> list:
"""Append a value to a list in memory"""
current = self.get(key, scope=scope, default=[])
if isinstance(current, list):
@@ -56,48 +52,48 @@ class MemoryHelper:
class MemoryStoreNode(WorkflowNode):
"""Memory store node - store and retrieve from workflow memory"""
type_name = "memory_store"
category = "integration"
icon = "HardDrive"
name = "memory_store"
description = "memory_store"
name_zh = "记忆存储"
name_en = "Memory Store"
description_zh = "从工作流记忆中存储和检索数据"
description_en = "Store and retrieve data from workflow memory"
type_name = 'memory_store'
category = 'integration'
icon = 'HardDrive'
name = 'memory_store'
description = 'memory_store'
name_zh = '记忆存储'
name_en = 'Memory Store'
description_zh = '从工作流记忆中存储和检索数据'
description_en = 'Store and retrieve data from workflow memory'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
operation = self.get_config("operation", "get")
key = self.get_config("key", "")
scope = self.get_config("scope", "execution")
ttl = self.get_config("ttl", 0)
operation = self.get_config('operation', 'get')
key = self.get_config('key', '')
scope = self.get_config('scope', 'execution')
ttl = self.get_config('ttl', 0)
value = inputs.get("value")
value = inputs.get('value')
# Wrap context.memory dict with MemoryHelper for structured operations
memory = MemoryHelper(context.memory)
try:
if operation == "get":
if operation == 'get':
result = memory.get(key, scope=scope)
return {"result": result, "success": True}
elif operation == "set":
return {'result': result, 'success': True}
elif operation == 'set':
memory.set(key, value, scope=scope, ttl=ttl)
return {"result": value, "success": True}
elif operation == "delete":
return {'result': value, 'success': True}
elif operation == 'delete':
memory.delete(key, scope=scope)
return {"result": None, "success": True}
elif operation == "append":
return {'result': None, 'success': True}
elif operation == 'append':
result = memory.append(key, value, scope=scope, ttl=ttl)
return {"result": result, "success": True}
elif operation == "list":
return {'result': result, 'success': True}
elif operation == 'list':
result = memory.list_all(scope=scope)
return {"result": result, "success": True}
return {'result': result, 'success': True}
else:
return {"result": None, "success": False, "error": f"Unknown operation: {operation}"}
return {'result': None, 'success': False, 'error': f'Unknown operation: {operation}'}
except Exception as e:
return {"result": None, "success": False, "error": str(e)}
return {'result': None, 'success': False, 'error': str(e)}

View File

@@ -15,51 +15,51 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class MergeNode(WorkflowNode):
"""Merge node - combine multiple inputs"""
type_name = "merge"
category = "control"
icon = "🔗"
name = "merge"
description = "merge"
name_zh = "合并"
name_en = "Merge"
description_zh = "将多个分支合并在一起"
description_en = "Merge multiple branches back together"
type_name = 'merge'
category = 'control'
icon = '🔗'
name = 'merge'
description = 'merge'
name_zh = '合并'
name_en = 'Merge'
description_zh = '将多个分支合并在一起'
description_en = 'Merge multiple branches back together'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
strategy = self.get_config("merge_strategy", "object")
strategy = self.get_config('merge_strategy', 'object')
values = [inputs.get("input_1"), inputs.get("input_2"), inputs.get("input_3"), inputs.get("input_4")]
values = [inputs.get('input_1'), inputs.get('input_2'), inputs.get('input_3'), inputs.get('input_4')]
non_null_values = [v for v in values if v is not None]
if strategy == "object":
if strategy == 'object':
merged = {}
for i, v in enumerate(non_null_values):
if isinstance(v, dict):
merged.update(v)
else:
merged[f"value_{i}"] = v
return {"merged": merged, "array": non_null_values}
merged[f'value_{i}'] = v
return {'merged': merged, 'array': non_null_values}
elif strategy == "array":
return {"merged": non_null_values, "array": non_null_values}
elif strategy == 'array':
return {'merged': non_null_values, 'array': non_null_values}
elif strategy == "first_non_null":
elif strategy == 'first_non_null':
first = non_null_values[0] if non_null_values else None
return {"merged": first, "array": non_null_values}
return {'merged': first, 'array': non_null_values}
elif strategy == "concat":
elif strategy == 'concat':
if all(isinstance(v, str) for v in non_null_values):
return {"merged": "".join(non_null_values), "array": non_null_values}
return {'merged': ''.join(non_null_values), 'array': non_null_values}
elif all(isinstance(v, list) for v in non_null_values):
merged_list = []
for v in non_null_values:
merged_list.extend(v)
return {"merged": merged_list, "array": merged_list}
return {'merged': merged_list, 'array': merged_list}
else:
return {"merged": non_null_values, "array": non_null_values}
return {'merged': non_null_values, 'array': non_null_values}
return {"merged": non_null_values, "array": non_null_values}
return {'merged': non_null_values, 'array': non_null_values}

View File

@@ -17,40 +17,40 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class MessageTriggerNode(WorkflowNode):
"""Message trigger node - triggers workflow on message arrival"""
type_name = "message_trigger"
category = "trigger"
icon = "💬"
name = "message_trigger"
description = "message_trigger"
name_zh = "消息触发"
name_en = "Message Trigger"
description_zh = "当收到消息时触发工作流"
description_en = "Trigger workflow when a message is received"
type_name = 'message_trigger'
category = 'trigger'
icon = '💬'
name = 'message_trigger'
description = 'message_trigger'
name_zh = '消息触发'
name_en = 'Message Trigger'
description_zh = '当收到消息时触发工作流'
description_en = 'Trigger workflow when a message is received'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
msg_ctx = context.message_context
if msg_ctx:
return {
"message": msg_ctx.message_content,
"sender_id": msg_ctx.sender_id,
"sender_name": msg_ctx.sender_name,
"platform": msg_ctx.platform,
"conversation_id": msg_ctx.conversation_id,
"is_group": msg_ctx.is_group,
"context": msg_ctx.model_dump(),
'message': msg_ctx.message_content,
'sender_id': msg_ctx.sender_id,
'sender_name': msg_ctx.sender_name,
'platform': msg_ctx.platform,
'conversation_id': msg_ctx.conversation_id,
'is_group': msg_ctx.is_group,
'context': msg_ctx.model_dump(),
}
return {
"message": context.get_variable("message", ""),
"sender_id": context.get_variable("sender_id", ""),
"sender_name": context.get_variable("sender_name", ""),
"platform": context.get_variable("platform", ""),
"conversation_id": context.get_variable("conversation_id", ""),
"is_group": context.get_variable("is_group", False),
"context": context.trigger_data,
'message': context.get_variable('message', ''),
'sender_id': context.get_variable('sender_id', ''),
'sender_name': context.get_variable('sender_name', ''),
'platform': context.get_variable('platform', ''),
'conversation_id': context.get_variable('conversation_id', ''),
'is_group': context.get_variable('is_group', False),
'context': context.trigger_data,
}

View File

@@ -15,33 +15,33 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class N8nWorkflowNode(WorkflowNode):
"""n8n workflow node - call n8n workflow API"""
type_name = "n8n_workflow"
category = "integration"
icon = "Workflow"
name = "n8n_workflow"
description = "n8n_workflow"
name_zh = "n8n 工作流"
name_en = "N8n Workflow"
description_zh = "通过 webhook 调用 n8n 工作流"
description_en = "Call an n8n workflow via webhook"
type_name = 'n8n_workflow'
category = 'integration'
icon = 'Workflow'
name = 'n8n_workflow'
description = 'n8n_workflow'
name_zh = 'n8n 工作流'
name_en = 'N8n Workflow'
description_zh = '通过 webhook 调用 n8n 工作流'
description_en = 'Call an n8n workflow via webhook'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
webhook_url = self.get_config("webhook_url", "")
auth_type = self.get_config("auth_type", "none")
timeout = self.get_config("timeout", 120)
payload = inputs.get("payload", {})
webhook_url = self.get_config('webhook_url', '')
auth_type = self.get_config('auth_type', 'none')
timeout = self.get_config('timeout', 120)
payload = inputs.get('payload', {})
return {
"result": None,
"success": False,
"_debug": {
"webhook_url": webhook_url,
"auth_type": auth_type,
"timeout": timeout,
"payload": payload,
'result': None,
'success': False,
'_debug': {
'webhook_url': webhook_url,
'auth_type': auth_type,
'timeout': timeout,
'payload': payload,
},
}

View File

@@ -15,23 +15,23 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class OpeningStatementNode(WorkflowNode):
"""Opening statement node - provide conversation opener and suggested questions"""
type_name = "opening_statement"
category = "action"
icon = "👋"
name = "opening_statement"
description = "opening_statement"
name_zh = "对话开场白"
name_en = "Opening Statement"
description_zh = "提供对话开场白和建议问题"
description_en = "Provide conversation opener and suggested questions"
type_name = 'opening_statement'
category = 'action'
icon = '👋'
name = 'opening_statement'
description = 'opening_statement'
name_zh = '对话开场白'
name_en = 'Opening Statement'
description_zh = '提供对话开场白和建议问题'
description_en = 'Provide conversation opener and suggested questions'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
statement = self.get_config("statement", "")
suggestions = self.get_config("suggested_questions", [])
show = self.get_config("show_suggestions", True)
statement = self.get_config('statement', '')
suggestions = self.get_config('suggested_questions', [])
show = self.get_config('show_suggestions', True)
return {"statement": statement, "suggested_questions": suggestions if show else []}
return {'statement': statement, 'suggested_questions': suggestions if show else []}

View File

@@ -12,38 +12,44 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class ParallelNode(WorkflowNode):
"""Parallel node - execute multiple branches simultaneously"""
type_name = "parallel"
category = "control"
icon = ""
name = "parallel"
name_zh = "并行执行"
name_en = "Parallel"
description = "parallel"
description_zh = "并行执行多个分支"
description_en = "Execute multiple branches in parallel"
type_name = 'parallel'
category = 'control'
icon = ''
name = 'parallel'
name_zh = '并行执行'
name_en = 'Parallel'
description = 'parallel'
description_zh = '并行执行多个分支'
description_en = 'Execute multiple branches in parallel'
inputs: ClassVar[list[NodePort]] = [
NodePort(name="input", type="any", description="Input data for all branches", required=False),
NodePort(name='input', type='any', description='Input data for all branches', required=False),
]
outputs: ClassVar[list[NodePort]] = [
NodePort(name="results", type="object", description="Combined results from all branches"),
NodePort(name="errors", type="array", description="Errors from branches (if any)"),
NodePort(name='results', type='object', description='Combined results from all branches'),
NodePort(name='errors', type='array', description='Errors from branches (if any)'),
]
config_schema: ClassVar[list[NodeConfig]] = [
NodeConfig(
name="wait_all", type="boolean", required=False, default=True,
description="Wait for all branches to complete",
label={"en_US": "Wait for All", "zh_Hans": "等待全部完成"},
name='wait_all',
type='boolean',
required=False,
default=True,
description='Wait for all branches to complete',
label={'en_US': 'Wait for All', 'zh_Hans': '等待全部完成'},
),
NodeConfig(
name="fail_fast", type="boolean", required=False, default=False,
description="Stop all branches if any fails",
label={"en_US": "Fail Fast", "zh_Hans": "快速失败"},
name='fail_fast',
type='boolean',
required=False,
default=False,
description='Stop all branches if any fails',
label={'en_US': 'Fail Fast', 'zh_Hans': '快速失败'},
),
]
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
return {
"results": {},
"errors": [],
'results': {},
'errors': [],
}

View File

@@ -15,25 +15,25 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class ParameterExtractorNode(WorkflowNode):
"""Parameter extractor node - extract structured parameters from text"""
type_name = "parameter_extractor"
category = "process"
icon: str = "📤"
name = "parameter_extractor"
description = "parameter_extractor"
name_zh = "参数提取器"
name_en = "Parameter Extractor"
description_zh = "使用 AI 从文本中提取结构化参数"
description_en = "Extract structured parameters from text using AI"
type_name = 'parameter_extractor'
category = 'process'
icon: str = '📤'
name = 'parameter_extractor'
description = 'parameter_extractor'
name_zh = '参数提取器'
name_en = 'Parameter Extractor'
description_zh = '使用 AI 从文本中提取结构化参数'
description_en = 'Extract structured parameters from text using AI'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
param_defs = self.get_config("parameters", [])
param_defs = self.get_config('parameters', [])
extracted = {}
for param in param_defs:
extracted[param.get("name", "")] = None
extracted[param.get('name', '')] = None
return {"parameters": extracted, "extraction_success": False}
return {'parameters': extracted, 'extraction_success': False}

View File

@@ -15,28 +15,28 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class QuestionClassifierNode(WorkflowNode):
"""Question classifier node - classify user questions into categories"""
type_name = "question_classifier"
category = "process"
icon = "🏷️"
name = "question_classifier"
description = "question_classifier"
name_zh = "问题分类器"
name_en = "Question Classifier"
description_zh = "使用 AI 将问题分类到预定义类别"
description_en = "Classify questions into predefined categories using AI"
type_name = 'question_classifier'
category = 'process'
icon = '🏷️'
name = 'question_classifier'
description = 'question_classifier'
name_zh = '问题分类器'
name_en = 'Question Classifier'
description_zh = '使用 AI 将问题分类到预定义类别'
description_en = 'Classify questions into predefined categories using AI'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
categories = self.get_config("categories", [])
categories = self.get_config('categories', [])
if categories:
return {
"category": categories[0].get("name", "unknown"),
"confidence": 0.8,
"all_scores": {cat.get("name"): 0.1 for cat in categories},
'category': categories[0].get('name', 'unknown'),
'confidence': 0.8,
'all_scores': {cat.get('name'): 0.1 for cat in categories},
}
return {"category": "unknown", "confidence": 0.0, "all_scores": {}}
return {'category': 'unknown', 'confidence': 0.0, 'all_scores': {}}

View File

@@ -15,39 +15,39 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class RedisOperationNode(WorkflowNode):
"""Redis operation node - perform Redis cache operations"""
type_name = "redis_operation"
category = "integration"
icon = "Server"
name = "redis_operation"
description = "redis_operation"
name_zh = "Redis 操作"
name_en = "Redis Operation"
description_zh = "执行 Redis 缓存操作"
description_en = "Perform Redis cache operations"
type_name = 'redis_operation'
category = 'integration'
icon = 'Server'
name = 'redis_operation'
description = 'redis_operation'
name_zh = 'Redis 操作'
name_en = 'Redis Operation'
description_zh = '执行 Redis 缓存操作'
description_en = 'Perform Redis cache operations'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
connection_url = self.get_config("connection_url", "redis://localhost:6379")
operation = self.get_config("operation", "get")
key_template = self.get_config("key_template", "")
hash_field = self.get_config("hash_field", "")
ttl = self.get_config("ttl", 0)
connection_url = self.get_config('connection_url', 'redis://localhost:6379')
operation = self.get_config('operation', 'get')
key_template = self.get_config('key_template', '')
hash_field = self.get_config('hash_field', '')
ttl = self.get_config('ttl', 0)
key = inputs.get("key", key_template)
value = inputs.get("value")
key = inputs.get('key', key_template)
value = inputs.get('value')
return {
"result": None,
"success": False,
"_debug": {
"connection_url": connection_url,
"operation": operation,
"key": key,
"hash_field": hash_field,
"ttl": ttl,
"value": value,
'result': None,
'success': False,
'_debug': {
'connection_url': connection_url,
'operation': operation,
'key': key,
'hash_field': hash_field,
'ttl': ttl,
'value': value,
},
}

View File

@@ -18,44 +18,44 @@ logger = logging.getLogger(__name__)
class ReplyMessageNode(WorkflowNode):
"""Reply message node - reply to the triggering message"""
type_name = "reply_message"
category = "action"
icon = "↩️"
name = "reply_message"
description = "reply_message"
name_zh = "回复消息"
name_en = "Reply Message"
description_zh = "回复触发工作流的消息"
description_en = "Reply to the message that triggered the workflow"
type_name = 'reply_message'
category = 'action'
icon = '↩️'
name = 'reply_message'
description = 'reply_message'
name_zh = '回复消息'
name_en = 'Reply Message'
description_zh = '回复触发工作流的消息'
description_en = 'Reply to the message that triggered the workflow'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
message = inputs.get("message")
if message in (None, ""):
message = inputs.get("input")
if message in (None, ""):
message = inputs.get("response")
if message in (None, ""):
message = inputs.get("content")
if message in (None, "") and context.message_context:
message = inputs.get('message')
if message in (None, ''):
message = inputs.get('input')
if message in (None, ''):
message = inputs.get('response')
if message in (None, ''):
message = inputs.get('content')
if message in (None, '') and context.message_context:
message = context.message_context.message_content
if message is None:
message = ""
message = ''
template = self.get_config("message_template")
template = self.get_config('message_template')
if template:
message = template
for key, value in inputs.items():
message = message.replace(f"{{{{{key}}}}}", str(value))
message = message.replace(f'{{{{{key}}}}}', str(value))
for key, value in context.variables.items():
message = message.replace(f"{{{{variables.{key}}}}}", str(value))
message = message.replace(f'{{{{variables.{key}}}}}', str(value))
logger.info(
"ReplyMessageNode resolved message",
'ReplyMessageNode resolved message',
extra={
'node_id': self.node_id,
'execution_id': context.execution_id,
@@ -68,7 +68,7 @@ class ReplyMessageNode(WorkflowNode):
if not str(message).strip():
logger.warning(
"ReplyMessageNode has empty message after resolution",
'ReplyMessageNode has empty message after resolution',
extra={
'node_id': self.node_id,
'execution_id': context.execution_id,
@@ -79,6 +79,7 @@ class ReplyMessageNode(WorkflowNode):
# 实际发送消息
if self.ap:
from langbot_plugin.api.entities.builtin.platform.message import MessageChain, Plain
message_chain = MessageChain([Plain(text=str(message))])
await self.ap.platform_mgr.websocket_proxy_bot.adapter.send_message(
target_type='person',
@@ -87,11 +88,11 @@ class ReplyMessageNode(WorkflowNode):
)
else:
logger.warning(
"ReplyMessageNode missing application instance",
'ReplyMessageNode missing application instance',
extra={
'node_id': self.node_id,
'execution_id': context.execution_id,
},
)
return {"status": "sent", "message_id": f"reply_{context.execution_id}"}
return {'status': 'sent', 'message_id': f'reply_{context.execution_id}'}

View File

@@ -15,19 +15,19 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class SendMessageNode(WorkflowNode):
"""Send message node - send message to a target"""
type_name = "send_message"
category = "action"
icon = "📤"
name = "send_message"
description = "send_message"
name_zh = "发送消息"
name_en = "Send Message"
description_zh = "向聊天或用户发送消息"
description_en = "Send a message to a chat or user"
type_name = 'send_message'
category = 'action'
icon = '📤'
name = 'send_message'
description = 'send_message'
name_zh = '发送消息'
name_en = 'Send Message'
description_zh = '向聊天或用户发送消息'
description_en = 'Send a message to a chat or user'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
return {"status": "sent", "message_id": f"msg_{context.execution_id}"}
return {'status': 'sent', 'message_id': f'msg_{context.execution_id}'}

View File

@@ -15,50 +15,50 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class SetVariableNode(WorkflowNode):
"""Set variable node - set workflow or conversation variable"""
type_name = "set_variable"
category = "action"
icon = "📝"
name = "set_variable"
description = "set_variable"
name_zh = "设置变量"
name_en = "Set Variable"
description_zh = "设置上下文变量值"
description_en = "Set a context variable value"
type_name = 'set_variable'
category = 'action'
icon = '📝'
name = 'set_variable'
description = 'set_variable'
name_zh = '设置变量'
name_en = 'Set Variable'
description_zh = '设置上下文变量值'
description_en = 'Set a context variable value'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
value = inputs.get("value")
name = self.get_config("variable_name", "")
scope = self.get_config("variable_scope", "workflow")
operation = self.get_config("operation", "set")
value = inputs.get('value')
name = self.get_config('variable_name', '')
scope = self.get_config('variable_scope', 'workflow')
operation = self.get_config('operation', 'set')
if scope == "conversation":
if scope == 'conversation':
current = context.get_conversation_variable(name)
else:
current = context.get_variable(name)
if operation == "set":
if operation == 'set':
final_value = value
elif operation == "append":
elif operation == 'append':
if isinstance(current, list):
final_value = current + [value]
elif isinstance(current, str):
final_value = current + str(value)
else:
final_value = [current, value] if current else [value]
elif operation == "increment":
elif operation == 'increment':
final_value = (current or 0) + (value if isinstance(value, (int, float)) else 1)
elif operation == "decrement":
elif operation == 'decrement':
final_value = (current or 0) - (value if isinstance(value, (int, float)) else 1)
else:
final_value = value
if scope == "conversation":
if scope == 'conversation':
context.set_conversation_variable(name, final_value)
else:
context.set_variable(name, final_value)
return {"value": final_value}
return {'value': final_value}

View File

@@ -15,31 +15,31 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class StoreDataNode(WorkflowNode):
"""Store data node - save data to storage"""
type_name = "store_data"
category = "action"
icon = "💾"
name = "store_data"
description = "store_data"
name_zh = "存储数据"
name_en = "Store Data"
description_zh = "将数据存储到持久化存储"
description_en = "Store data to persistent storage"
type_name = 'store_data'
category = 'action'
icon = '💾'
name = 'store_data'
description = 'store_data'
name_zh = '存储数据'
name_en = 'Store Data'
description_zh = '将数据存储到持久化存储'
description_en = 'Store data to persistent storage'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
key = inputs.get("key", "")
value = inputs.get("value")
storage_type = self.get_config("storage_type", "session")
prefix = self.get_config("key_prefix", "")
key = inputs.get('key', '')
value = inputs.get('value')
storage_type = self.get_config('storage_type', 'session')
prefix = self.get_config('key_prefix', '')
full_key = f"{prefix}{key}" if prefix else key
full_key = f'{prefix}{key}' if prefix else key
if storage_type == "session":
if storage_type == 'session':
context.set_conversation_variable(full_key, value)
else:
context.set_variable(full_key, value)
return {"status": "stored"}
return {'status': 'stored'}

View File

@@ -15,42 +15,42 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class SwitchNode(WorkflowNode):
"""Switch node - multi-way branch based on value"""
type_name = "switch"
category = "control"
icon = "🔃"
name = "switch"
description = "switch"
name_zh = "多路分支"
name_en = "Switch"
description_zh = "根据多个条件分支工作流"
description_en = "Branch workflow based on multiple cases"
type_name = 'switch'
category = 'control'
icon = '🔃'
name = 'switch'
description = 'switch'
name_zh = '多路分支'
name_en = 'Switch'
description_zh = '根据多个条件分支工作流'
description_en = 'Branch workflow based on multiple cases'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
expression = self.get_config("expression", "")
cases = self.get_config("cases", [])
input_data = inputs.get("input")
expression = self.get_config('expression', '')
cases = self.get_config('cases', [])
input_data = inputs.get('input')
value = await self._evaluate_expression(expression, input_data, context)
for case in cases:
if str(case.get("value")) == str(value):
return {"matched_case": input_data, "default": None, "_matched_output": case.get("output")}
if str(case.get('value')) == str(value):
return {'matched_case': input_data, 'default': None, '_matched_output': case.get('output')}
return {"matched_case": None, "default": input_data}
return {'matched_case': None, 'default': input_data}
async def _evaluate_expression(self, expression: str, data: Any, context: ExecutionContext) -> Any:
if not expression:
return data
if expression.startswith("{{") and expression.endswith("}}"):
if expression.startswith('{{') and expression.endswith('}}'):
var_path = expression[2:-2].strip()
parts = var_path.split(".")
parts = var_path.split('.')
if parts[0] == "input":
if parts[0] == 'input':
result = data
for part in parts[1:]:
if isinstance(result, dict):
@@ -58,7 +58,7 @@ class SwitchNode(WorkflowNode):
else:
return None
return result
elif parts[0] == "variables":
return context.variables.get(".".join(parts[1:]))
elif parts[0] == 'variables':
return context.variables.get('.'.join(parts[1:]))
return expression

View File

@@ -15,37 +15,37 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class VariableAggregatorNode(WorkflowNode):
"""Variable aggregator node - aggregate variables from multiple branches"""
type_name = "variable_aggregator"
category = "control"
icon = "📊"
name = "variable_aggregator"
description = "variable_aggregator"
name_zh = "变量聚合器"
name_en = "Variable Aggregator"
description_zh = "聚合多个分支的变量输出"
description_en = "Aggregate variable outputs from multiple branches"
type_name = 'variable_aggregator'
category = 'control'
icon = '📊'
name = 'variable_aggregator'
description = 'variable_aggregator'
name_zh = '变量聚合器'
name_en = 'Variable Aggregator'
description_zh = '聚合多个分支的变量输出'
description_en = 'Aggregate variable outputs from multiple branches'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
variables = inputs.get("variables", {})
mode = self.get_config("aggregation_mode", "merge")
variables = inputs.get('variables', {})
mode = self.get_config('aggregation_mode', 'merge')
aggregated = {}
if mode == "merge":
if mode == 'merge':
if isinstance(variables, dict):
aggregated.update(variables)
elif mode == "override":
elif mode == 'override':
if isinstance(variables, dict):
aggregated = variables.copy()
elif mode == "append":
elif mode == 'append':
for key, value in (variables if isinstance(variables, dict) else {}).items():
if key in aggregated and isinstance(aggregated[key], list):
aggregated[key].append(value)
else:
aggregated[key] = [value]
return {"aggregated": aggregated}
return {'aggregated': aggregated}

View File

@@ -15,15 +15,15 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class WaitNode(WorkflowNode):
"""Wait node - pause execution for a duration"""
type_name = "wait"
category = "control"
icon = ""
name = "wait"
description = "wait"
name_zh = "等待"
name_en = "Wait"
description_zh = "暂停工作流执行指定时间"
description_en = "Pause workflow execution for a specified duration"
type_name = 'wait'
category = 'control'
icon = ''
name = 'wait'
description = 'wait'
name_zh = '等待'
name_en = 'Wait'
description_zh = '暂停工作流执行指定时间'
description_en = 'Pause workflow execution for a specified duration'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
@@ -32,14 +32,14 @@ class WaitNode(WorkflowNode):
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
import asyncio
duration = self.get_config("duration", 1)
duration_type = self.get_config("duration_type", "seconds")
duration = self.get_config('duration', 1)
duration_type = self.get_config('duration_type', 'seconds')
if duration_type == "minutes":
if duration_type == 'minutes':
duration *= 60
elif duration_type == "hours":
elif duration_type == 'hours':
duration *= 3600
await asyncio.sleep(duration)
return {"output": inputs.get("input")}
return {'output': inputs.get('input')}

View File

@@ -15,15 +15,15 @@ from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
class WebhookTriggerNode(WorkflowNode):
"""Webhook trigger node - triggers workflow via HTTP request"""
type_name = "webhook_trigger"
category = "trigger"
icon = "🌐"
name = "webhook_trigger"
description = "webhook_trigger"
name_zh = "Webhook 触发"
name_en = "Webhook Trigger"
description_zh = "通过 HTTP 请求触发工作流"
description_en = "Trigger workflow via HTTP webhook"
type_name = 'webhook_trigger'
category = 'trigger'
icon = '🌐'
name = 'webhook_trigger'
description = 'webhook_trigger'
name_zh = 'Webhook 触发'
name_en = 'Webhook Trigger'
description_zh = '通过 HTTP 请求触发工作流'
description_en = 'Trigger workflow via HTTP webhook'
inputs: ClassVar[list[NodePort]] = []
outputs: ClassVar[list[NodePort]] = []
@@ -33,8 +33,8 @@ class WebhookTriggerNode(WorkflowNode):
trigger_data = context.trigger_data
return {
"body": trigger_data.get("body", {}),
"headers": trigger_data.get("headers", {}),
"query": trigger_data.get("query", {}),
"method": trigger_data.get("method", "POST"),
'body': trigger_data.get('body', {}),
'headers': trigger_data.get('headers', {}),
'query': trigger_data.get('query', {}),
'method': trigger_data.get('method', 'POST'),
}