This commit is contained in:
Typer_Body
2026-05-18 01:47:13 +08:00
parent 27c0d344bf
commit bb7db53447
89 changed files with 1202 additions and 6883 deletions

View File

@@ -567,58 +567,30 @@ class WorkflowService:
return result
def _enrich_node_type_configs(self, node_types: list[dict]) -> list[dict]:
"""Enrich node schemas with node YAML config metadata if available"""
# Get workflow node configs loaded from YAML files
workflow_node_configs = getattr(self.ap, 'workflow_node_configs', {})
"""Enrich node schemas with pipeline config metadata if available.
Dedicated workflow node YAML metadata is already applied by
NodeTypeRegistry before this method runs. This method only keeps the
older pipeline metadata reuse path for nodes that explicitly request it.
"""
for node_schema in node_types:
node_type = node_schema.get('type', '')
node_schema.setdefault('config_schema', [])
# First, try to load config from dedicated node YAML file
if node_type in workflow_node_configs:
node_config = workflow_node_configs[node_type]
# Enrich inputs from YAML if Python class has empty inputs
yaml_inputs = node_config.get('inputs', [])
if yaml_inputs and not node_schema.get('inputs'):
node_schema['inputs'] = [self._normalize_port_item(inp) for inp in yaml_inputs]
# Enrich outputs from YAML if Python class has empty outputs
yaml_outputs = node_config.get('outputs', [])
if yaml_outputs and not node_schema.get('outputs'):
node_schema['outputs'] = [self._normalize_port_item(out) for out in yaml_outputs]
# Enrich config_schema from YAML
yaml_configs = node_config.get('config', [])
# Normalize and add YAML configs to config_schema
existing_names = {cfg.get('name') for cfg in node_schema.get('config_schema', [])}
for cfg in yaml_configs:
if cfg.get('name') not in existing_names:
normalized_cfg = self._normalize_config_item(cfg)
node_schema['config_schema'].append(normalized_cfg)
existing_names.add(cfg.get('name'))
# Second, try to load from pipeline config metadata (for backward compatibility)
config_schema_source = node_schema.get('config_schema_source')
config_stages = node_schema.get('config_stages', [])
if config_schema_source and config_stages:
# Get pipeline config metadata
pipeline_meta = self._get_pipeline_config_meta(config_schema_source)
if pipeline_meta:
# Extract config items from specified stages
enriched_configs = []
for stage_name in config_stages:
stage_data = self._find_stage_in_pipeline(pipeline_meta, stage_name)
if stage_data and 'config' in stage_data:
enriched_configs.extend(stage_data['config'])
# Merge with existing config_schema (avoid duplicates by name)
existing_names = {cfg.get('name') for cfg in node_schema.get('config_schema', [])}
for cfg in enriched_configs:
if cfg.get('name') not in existing_names:
# Normalize config item format for frontend compatibility
normalized_cfg = self._normalize_config_item(cfg)
node_schema['config_schema'].append(normalized_cfg)
existing_names.add(cfg.get('name'))

View File

@@ -222,51 +222,35 @@ class LoadConfigStage(stage.BootingStage):
ap.pipeline_config_meta_ai = await load_resource_yaml_template_data('metadata/pipeline/ai.yaml')
ap.pipeline_config_meta_output = await load_resource_yaml_template_data('metadata/pipeline/output.yaml')
# Load workflow node configurations from YAML files
ap.workflow_node_configs = {}
node_config_files = [
# Trigger nodes
'metadata/nodes/message_trigger.yaml',
'metadata/nodes/cron_trigger.yaml',
'metadata/nodes/webhook_trigger.yaml',
'metadata/nodes/event_trigger.yaml',
# AI/Process nodes
'metadata/nodes/llm_call.yaml',
'metadata/nodes/question_classifier.yaml',
'metadata/nodes/parameter_extractor.yaml',
'metadata/nodes/knowledge_retrieval.yaml',
'metadata/nodes/code_executor.yaml',
'metadata/nodes/data_transform.yaml',
# Control nodes
'metadata/nodes/condition.yaml',
'metadata/nodes/switch.yaml',
'metadata/nodes/loop.yaml',
'metadata/nodes/parallel.yaml',
'metadata/nodes/wait.yaml',
'metadata/nodes/end.yaml',
# Action nodes
'metadata/nodes/send_message.yaml',
'metadata/nodes/http_request.yaml',
# Integration nodes - Data & Tools
'metadata/nodes/database_query.yaml',
'metadata/nodes/redis_operation.yaml',
'metadata/nodes/mcp_tool.yaml',
'metadata/nodes/memory_store.yaml',
# Integration nodes - External services
'metadata/nodes/dify_workflow.yaml',
'metadata/nodes/dify_knowledge_query.yaml',
'metadata/nodes/n8n_workflow.yaml',
'metadata/nodes/langflow_flow.yaml',
'metadata/nodes/coze_bot.yaml',
]
for config_file in node_config_files:
try:
node_config = await load_resource_yaml_template_data(config_file)
node_name = node_config.get('name')
node_category = node_config.get('category', 'misc')
if node_name:
# Use category.name format to match node type format (e.g., integration.coze_bot)
full_type = f'{node_category}.{node_name}'
ap.workflow_node_configs[full_type] = node_config
except Exception as e:
print(f'Failed to load node config {config_file}: {e}')
# Load workflow node metadata from YAML files. YAML is the source of
# truth for workflow editor metadata; Python classes provide execution
# logic and are bound through the registry.
from langbot.pkg.workflow.metadata import NodeMetadataLoader
from langbot.pkg.workflow.registry import NodeTypeRegistry
workflow_metadata_loader = NodeMetadataLoader()
workflow_node_count = await workflow_metadata_loader.load_core_metadata()
ap.workflow_node_configs = workflow_metadata_loader.get_all_metadata()
ap.workflow_node_metadata_loader = workflow_metadata_loader
workflow_registry = NodeTypeRegistry.instance()
for node_config in ap.workflow_node_configs.values():
workflow_registry.register_metadata(node_config, source=node_config.get('_source', 'core'))
# Import node modules after metadata registration so decorators can bind
# implementations to YAML-defined canonical node types.
from langbot.pkg.workflow import nodes as workflow_nodes # noqa: F401
workflow_registry.process_pending_registrations()
workflow_load_errors = workflow_metadata_loader.get_load_errors()
if workflow_load_errors:
print(f'Workflow node metadata load errors: {len(workflow_load_errors)}')
for error in workflow_load_errors:
print(f" - {error.get('file')}: {error.get('error')}")
print(
f'Loaded {workflow_node_count} workflow node metadata files; '
f'registered {workflow_registry.metadata_count()} metadata definitions, '
f'{workflow_registry.count()} node types'
)

View File

@@ -21,12 +21,33 @@ from .entities import (
NodeStatus,
)
from .node import WorkflowNode, NodePort, NodeConfig, workflow_node
from .registry import NodeTypeRegistry
from .executor import WorkflowExecutor
from importlib import import_module
from typing import Any
# Import nodes module to trigger node registration
from . import nodes as nodes
from .node import WorkflowNode, workflow_node
def __getattr__(name: str) -> Any:
"""Lazily expose heavier workflow modules.
Loading workflow metadata should not import the executor or node modules as a
side effect. Node implementations are imported explicitly during boot after
YAML metadata has been registered.
"""
if name == 'NodeTypeRegistry':
from .registry import NodeTypeRegistry
return NodeTypeRegistry
if name == 'WorkflowExecutor':
from .executor import WorkflowExecutor
return WorkflowExecutor
if name == 'nodes':
return import_module('.nodes', __name__)
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
__all__ = [
# Entities
@@ -43,8 +64,6 @@ __all__ = [
'NodeStatus',
# Node
'WorkflowNode',
'NodePort',
'NodeConfig',
'workflow_node',
# Registry
'NodeTypeRegistry',

View File

@@ -0,0 +1,283 @@
"""Workflow node metadata loading and validation.
This module makes YAML files under ``templates/metadata/nodes`` the backend
source of truth for workflow node metadata. Python node classes still provide
execution logic, but UI-facing metadata is loaded from YAML.
"""
from __future__ import annotations
import copy
import logging
from importlib import resources
from pathlib import Path
from typing import Any, Iterable, Optional
import yaml
logger = logging.getLogger(__name__)
class MetadataLoadError(Exception):
"""Raised when a workflow node metadata file cannot be loaded."""
class MetadataValidationError(Exception):
"""Raised when workflow node metadata does not match the expected shape."""
class NodeMetadataValidator:
"""Validate workflow node metadata loaded from YAML files.
The validator is intentionally strict about the structural fields that the
editor needs, but tolerant of legacy YAML details such as missing top-level
``label`` or additional frontend field types.
"""
REQUIRED_FIELDS = ('name', 'category', 'inputs', 'outputs', 'config')
VALID_CATEGORIES = {'trigger', 'process', 'control', 'action', 'integration', 'misc'}
VALID_PORT_TYPES = {'any', 'string', 'number', 'integer', 'boolean', 'object', 'array', 'datetime', 'null'}
VALID_CONFIG_TYPES = {
'string',
'integer',
'number',
'float',
'boolean',
'select',
'json',
'textarea',
'text',
'secret',
'array[string]',
'file',
'array[file]',
'llm-model-selector',
'embedding-model-selector',
'rerank-model-selector',
'pipeline-selector',
'knowledge-base-selector',
'knowledge-base-multi-selector',
'bot-selector',
'tools-selector',
'model-fallback-selector',
'prompt-editor',
'plugin-selector',
'webhook-url',
'embed-code',
}
def validate(self, metadata: dict[str, Any]) -> list[str]:
"""Return validation errors. An empty list means the metadata is valid."""
errors: list[str] = []
if not isinstance(metadata, dict):
return ['metadata root must be a mapping']
for field in self.REQUIRED_FIELDS:
if field not in metadata:
errors.append(f'missing required field: {field}')
if errors:
return errors
name = metadata.get('name')
if not isinstance(name, str) or not name.strip():
errors.append('field "name" must be a non-empty string')
category = metadata.get('category')
if category not in self.VALID_CATEGORIES:
errors.append(f'invalid category: {category}')
errors.extend(self._validate_ports(metadata.get('inputs'), 'inputs'))
errors.extend(self._validate_ports(metadata.get('outputs'), 'outputs'))
errors.extend(self._validate_config(metadata.get('config')))
return errors
def validate_or_raise(self, metadata: dict[str, Any]) -> dict[str, Any]:
"""Validate metadata and raise ``MetadataValidationError`` on failure."""
errors = self.validate(metadata)
if errors:
node_name = metadata.get('name', 'unknown') if isinstance(metadata, dict) else 'unknown'
raise MetadataValidationError(f'invalid metadata for {node_name}: {errors}')
return metadata
def _validate_ports(self, ports: Any, field_name: str) -> list[str]:
errors: list[str] = []
if not isinstance(ports, list):
return [f'{field_name} must be a list']
seen_names: set[str] = set()
for index, port in enumerate(ports):
path = f'{field_name}[{index}]'
if not isinstance(port, dict):
errors.append(f'{path} must be a mapping')
continue
name = port.get('name')
if not isinstance(name, str) or not name:
errors.append(f'{path}.name must be a non-empty string')
continue
if name in seen_names:
errors.append(f'{path}.name duplicates "{name}"')
seen_names.add(name)
port_type = port.get('type', 'any')
if port_type not in self.VALID_PORT_TYPES:
errors.append(f'{path}.type has unsupported value "{port_type}"')
return errors
def _validate_config(self, config: Any) -> list[str]:
errors: list[str] = []
if not isinstance(config, list):
return ['config must be a list']
seen_names: set[str] = set()
for index, item in enumerate(config):
path = f'config[{index}]'
if not isinstance(item, dict):
errors.append(f'{path} must be a mapping')
continue
name = item.get('name')
if not isinstance(name, str) or not name:
errors.append(f'{path}.name must be a non-empty string')
continue
if name in seen_names:
errors.append(f'{path}.name duplicates "{name}"')
seen_names.add(name)
item_type = item.get('type', 'string')
if item_type not in self.VALID_CONFIG_TYPES:
errors.append(f'{path}.type has unsupported value "{item_type}"')
min_value = item.get('min_value')
max_value = item.get('max_value')
if isinstance(min_value, (int, float)) and isinstance(max_value, (int, float)) and min_value > max_value:
errors.append(f'{path}.min_value must be <= max_value')
return errors
class NodeMetadataLoader:
"""Load and cache workflow node metadata from YAML files."""
def __init__(self, validator: Optional[NodeMetadataValidator] = None) -> None:
self._validator = validator or NodeMetadataValidator()
self._metadata: dict[str, dict[str, Any]] = {}
self._sources: dict[str, str] = {}
self._load_errors: list[dict[str, str]] = []
async def load_core_metadata(self, resource_dir: str = 'metadata/nodes') -> int:
"""Load all core node metadata from the ``langbot.templates`` package."""
return await self.load_package_directory('langbot.templates', resource_dir, source='core')
async def load_package_directory(self, package: str, resource_dir: str, source: str = 'core') -> int:
"""Load YAML files from a package resource directory."""
try:
root = resources.files(package).joinpath(resource_dir)
yaml_files = sorted(
(item for item in root.iterdir() if item.is_file() and item.name.endswith(('.yaml', '.yml'))),
key=lambda item: item.name,
)
except Exception as exc:
raise MetadataLoadError(f'failed to scan package directory {package}:{resource_dir}: {exc}') from exc
return self._load_files(yaml_files, source=source)
async def load_directory(self, directory: str | Path, source: str) -> int:
"""Load YAML files from an external filesystem directory, e.g. a plugin."""
directory_path = Path(directory)
if not directory_path.exists():
logger.warning('Workflow metadata directory does not exist: %s', directory_path)
return 0
if not directory_path.is_dir():
raise MetadataLoadError(f'workflow metadata path is not a directory: {directory_path}')
yaml_files = sorted(directory_path.glob('*.yml')) + sorted(directory_path.glob('*.yaml'))
return self._load_files(yaml_files, source=source)
def get_metadata(self, node_type: str) -> Optional[dict[str, Any]]:
"""Return metadata by full type or short node name."""
if node_type in self._metadata:
return copy.deepcopy(self._metadata[node_type])
short_name = node_type.split('.')[-1]
for registered_type, metadata in self._metadata.items():
if registered_type.split('.')[-1] == short_name or metadata.get('name') == short_name:
return copy.deepcopy(metadata)
return None
def get_all_metadata(self) -> dict[str, dict[str, Any]]:
"""Return a deep copy of all loaded metadata keyed by canonical node type."""
return copy.deepcopy(self._metadata)
def get_load_errors(self) -> list[dict[str, str]]:
"""Return metadata files that failed to load or validate."""
return copy.deepcopy(self._load_errors)
def clear(self) -> None:
"""Clear all cached metadata and errors."""
self._metadata.clear()
self._sources.clear()
self._load_errors.clear()
def _load_files(self, yaml_files: Iterable[Any], source: str) -> int:
count = 0
for yaml_file in yaml_files:
file_name = getattr(yaml_file, 'name', str(yaml_file))
try:
metadata = self._load_yaml(yaml_file)
self._validator.validate_or_raise(metadata)
node_type = build_node_type(metadata)
if node_type in self._metadata:
existing_source = self._sources.get(node_type, 'unknown')
if existing_source == 'core' and source != 'core':
raise MetadataLoadError(
f'plugin source "{source}" attempted to override core node "{node_type}"'
)
logger.warning(
'Workflow node metadata %s from %s overrides previous source %s',
node_type,
source,
existing_source,
)
cached_metadata = copy.deepcopy(metadata)
cached_metadata['_source'] = source
cached_metadata['_file'] = file_name
self._metadata[node_type] = cached_metadata
self._sources[node_type] = source
count += 1
except Exception as exc:
self._load_errors.append({'file': file_name, 'source': source, 'error': str(exc)})
logger.error('Failed to load workflow node metadata %s: %s', file_name, exc)
return count
def _load_yaml(self, yaml_file: Any) -> dict[str, Any]:
try:
if hasattr(yaml_file, 'open'):
with yaml_file.open('r', encoding='utf-8') as file:
data = yaml.load(file, Loader=yaml.FullLoader)
else:
with open(yaml_file, 'r', encoding='utf-8') as file:
data = yaml.load(file, Loader=yaml.FullLoader)
except Exception as exc:
raise MetadataLoadError(f'failed to parse YAML: {exc}') from exc
if not isinstance(data, dict):
raise MetadataLoadError('YAML root must be a mapping')
return data
def build_node_type(metadata: dict[str, Any]) -> str:
"""Build canonical ``category.name`` node type from metadata."""
category = metadata.get('category') or 'misc'
name = metadata.get('name') or ''
return f'{category}.{name}'

View File

@@ -5,83 +5,38 @@ from __future__ import annotations
import abc
from typing import Any, Callable, Optional, TYPE_CHECKING
import pydantic
if TYPE_CHECKING:
from .entities import ExecutionContext
from ..core import app
class NodePort(pydantic.BaseModel):
"""Node port definition"""
name: str
type: str = 'any' # any, string, number, boolean, object, array
description: str = ''
required: bool = True
class NodeConfig(pydantic.BaseModel):
"""Node configuration field definition"""
name: str
type: str # string, integer, number, boolean, select, json, secret, etc.
required: bool = False
default: Any = None
description: str = ''
options: Optional[list[str]] = None # For select type
# Validation
min_value: Optional[float] = None
max_value: Optional[float] = None
min_length: Optional[int] = None
max_length: Optional[int] = None
pattern: Optional[str] = None # Regex pattern
# UI hints
placeholder: str = ''
show_if: Optional[dict] = None # Conditional display
# Pipeline config source (for reusing Pipeline config metadata)
pipeline_config_source: Optional[str] = None # e.g., "pipeline:trigger"
# i18n support for label
label: Optional[dict[str, str]] = None # e.g., {"en_US": "Name", "zh_Hans": "名称"}
label_zh: Optional[str] = None # Chinese label
label_en: Optional[str] = None # English label
class WorkflowNode(abc.ABC):
"""Base class for all workflow nodes"""
"""Base class for all workflow nodes.
# Node metadata
Node metadata (inputs, outputs, config schema, label, icon, etc.) is
defined exclusively in YAML files under templates/metadata/nodes/.
Python subclasses only provide execution logic and runtime behaviour.
"""
# Set by @workflow_node decorator
type_name: str = ''
name: str = ''
description: str = ''
category: str = 'misc' # trigger, process, control, action, integration
icon: str = ''
# Port definitions
inputs: list[NodePort] = []
outputs: list[NodePort] = []
# Category is kept as a fallback for registry when YAML is missing
category: str = 'misc'
# Configuration schema
config_schema: list[NodeConfig] = []
# Pipeline config reuse
config_schema_source: Optional[str] = None # e.g., "pipeline:ai"
config_stages: list[str] = [] # Specific stages to reuse
# Pipeline config reuse (referenced by registry merge logic)
config_schema_source: Optional[str] = None
config_stages: list[str] = []
def __init__(self, node_id: str, config: dict[str, Any], ap: Optional['app.Application'] = None):
"""Initialize node with ID and configuration"""
self.node_id = node_id
self.config = config
self.ap = ap # Reference to the application instance for accessing services
self.ap = ap
@abc.abstractmethod
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
"""
Execute the node logic.
"""Execute the node logic.
Args:
inputs: Input data from connected nodes
@@ -92,171 +47,87 @@ class WorkflowNode(abc.ABC):
"""
pass
# ------------------------------------------------------------------
# Validation helpers — metadata is resolved from the registry at
# runtime so that YAML remains the single source of truth.
# ------------------------------------------------------------------
async def validate_inputs(self, inputs: dict[str, Any]) -> list[str]:
"""
Validate input data against port definitions.
"""Validate input data against YAML port definitions.
Returns:
List of validation error messages (empty if valid)
"""
errors = []
for port in self.inputs:
if port.required and port.name not in inputs:
errors.append(f'Missing required input: {port.name}')
metadata = self._get_metadata()
if metadata is None:
return []
errors: list[str] = []
for port in metadata.get('inputs', []):
if port.get('required', True) and port.get('name') and port['name'] not in inputs:
errors.append(f"Missing required input: {port['name']}")
return errors
async def validate_config(self) -> list[str]:
"""
Validate node configuration.
"""Validate node configuration against YAML config schema.
Returns:
List of validation error messages (empty if valid)
"""
errors = []
for cfg in self.config_schema:
if cfg.required and cfg.name not in self.config:
errors.append(f'Missing required config: {cfg.name}')
elif cfg.name in self.config:
value = self.config[cfg.name]
# Type validation
if cfg.type == 'integer' and not isinstance(value, int):
errors.append(f'Config {cfg.name} must be an integer')
elif cfg.type == 'number' and not isinstance(value, (int, float)):
errors.append(f'Config {cfg.name} must be a number')
elif cfg.type == 'boolean' and not isinstance(value, bool):
errors.append(f'Config {cfg.name} must be a boolean')
# Range validation
if cfg.min_value is not None and isinstance(value, (int, float)):
if value < cfg.min_value:
errors.append(f'Config {cfg.name} must be >= {cfg.min_value}')
if cfg.max_value is not None and isinstance(value, (int, float)):
if value > cfg.max_value:
errors.append(f'Config {cfg.name} must be <= {cfg.max_value}')
return errors
metadata = self._get_metadata()
if metadata is None:
return []
# Type mapping from backend to frontend DynamicFormItemType
_TYPE_MAP = {
'string': 'string',
'integer': 'integer',
'number': 'float',
'boolean': 'boolean',
'select': 'select',
'json': 'text',
'textarea': 'text',
'secret': 'secret',
'llm-model-selector': 'llm-model-selector',
'embedding-model-selector': 'embedding-model-selector',
'rerank-model-selector': 'rerank-model-selector',
'pipeline-selector': 'pipeline-selector',
'knowledge-base-selector': 'knowledge-base-selector',
'knowledge-base-multi-selector': 'knowledge-base-multi-selector',
'bot-selector': 'bot-selector',
'tools-selector': 'tools-selector',
'model-fallback-selector': 'model-fallback-selector',
'prompt-editor': 'prompt-editor',
}
errors: list[str] = []
for cfg in metadata.get('config', []):
name = cfg.get('name', '')
if not name:
continue
required = cfg.get('required', False)
cfg_type = cfg.get('type', 'string')
if required and name not in self.config:
errors.append(f'Missing required config: {name}')
elif name in self.config:
value = self.config[name]
# Type validation
if cfg_type == 'integer' and not isinstance(value, int):
errors.append(f'Config {name} must be an integer')
elif cfg_type == 'number' and not isinstance(value, (int, float)):
errors.append(f'Config {name} must be a number')
elif cfg_type == 'boolean' and not isinstance(value, bool):
errors.append(f'Config {name} must be a boolean')
# Range validation
min_val = cfg.get('min_value')
max_val = cfg.get('max_value')
if min_val is not None and isinstance(value, (int, float)):
if value < min_val:
errors.append(f'Config {name} must be >= {min_val}')
if max_val is not None and isinstance(value, (int, float)):
if value > max_val:
errors.append(f'Config {name} must be <= {max_val}')
return errors
def get_config(self, key: str, default: Any = None) -> Any:
"""Get configuration value with default"""
return self.config.get(key, default)
@classmethod
def _config_to_schema_item(cls, cfg: NodeConfig) -> dict[str, Any]:
"""Convert a NodeConfig to frontend-compatible schema item"""
# Map type to frontend type
frontend_type = cls._TYPE_MAP.get(cfg.type, 'string')
# Build i18n label from name
label = {
'zh_Hans': cfg.name,
'en_US': cfg.name,
}
# Build i18n description
desc = cfg.description or ''
description = {
'zh_Hans': desc,
'en_US': desc,
}
result = {
'id': cfg.name,
'name': cfg.name,
'type': frontend_type,
'label': label,
'description': description,
'required': cfg.required,
'default': cfg.default,
}
# Add placeholder if present
if cfg.placeholder:
result['placeholder'] = cfg.placeholder
# Add options if present
if cfg.options:
result['options'] = [
{
'name': opt,
'label': {
'zh_Hans': opt,
'en_US': opt,
},
}
for opt in cfg.options
]
# Add show_if if present
if cfg.show_if:
result['show_if'] = cfg.show_if
return result
@classmethod
def to_schema(cls) -> dict[str, Any]:
"""
Convert node class to JSON schema for frontend.
Returns:
Node schema dictionary
"""
# Build label dict for i18n support
# Use underscore format to match frontend I18nObject interface
name_zh = getattr(cls, 'name_zh', None) or cls.name
name_en = getattr(cls, 'name_en', None) or cls.name
desc_zh = getattr(cls, 'description_zh', None) or cls.description
desc_en = getattr(cls, 'description_en', None) or cls.description
label = {
'zh_Hans': name_zh,
'en_US': name_en,
}
description = {
'zh_Hans': desc_zh,
'en_US': desc_en,
}
return {
'type': f'{cls.category}.{cls.type_name}',
'name': cls.name,
'label': label,
'description': description,
'category': cls.category,
'icon': cls.icon,
'inputs': [port.model_dump() for port in cls.inputs],
'outputs': [port.model_dump() for port in cls.outputs],
'config_schema': [cls._config_to_schema_item(cfg) for cfg in cls.config_schema],
'config_schema_source': cls.config_schema_source,
'config_stages': cls.config_stages,
}
def _get_metadata(self) -> Optional[dict[str, Any]]:
"""Retrieve YAML metadata for this node from the registry."""
from .registry import NodeTypeRegistry
registry = NodeTypeRegistry.instance()
return registry.get_metadata(self.type_name)
# Registry for node type decorator
# ------------------------------------------------------------------
# Decorator and pending registration helpers
# ------------------------------------------------------------------
_pending_registrations: list[tuple[str, type[WorkflowNode]]] = []
def workflow_node(type_name: str) -> Callable[[type[WorkflowNode]], type[WorkflowNode]]:
"""
Decorator to register a workflow node type.
"""Decorator to register a workflow node type.
Usage:
@workflow_node('llm_call')

View File

@@ -5,7 +5,7 @@ Node metadata is loaded from: ../../templates/metadata/nodes/call_pipeline.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
@@ -15,26 +15,13 @@ import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.entities.builtin.provider.session as provider_session
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('call_pipeline')
class CallPipelineNode(WorkflowNode):
"""Call pipeline node - invoke an existing pipeline"""
type_name = 'call_pipeline'
category = 'action'
icon = 'Workflow'
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]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
if not self.ap:
@@ -124,7 +111,6 @@ class CallPipelineNode(WorkflowNode):
if context.message_context and context.message_context.is_group:
group = platform_entities.Group(
id=context.message_context.group_id or context.session_id or 'workflow_group',
name='Workflow Group',
permission=platform_entities.Permission.Member,
)
sender = platform_entities.GroupMember(
@@ -152,7 +138,6 @@ class CallPipelineNode(WorkflowNode):
else None,
)
class _WorkflowPipelineCaptureAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
responses: list[dict[str, Any]] = []

View File

@@ -7,29 +7,16 @@ from __future__ import annotations
import json
import re
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('code_executor')
class CodeExecutorNode(WorkflowNode):
"""Code executor node - run Python or JavaScript code"""
type_name = 'code_executor'
category = 'process'
icon = 'Code'
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', '')

View File

@@ -5,30 +5,17 @@ Node metadata is loaded from: ../../templates/metadata/nodes/condition.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
from ..safe_eval import safe_eval_with_vars
@workflow_node('condition')
class ConditionNode(WorkflowNode):
"""Condition node - branch based on condition"""
type_name = 'condition'
category = 'control'
icon = 'GitBranch'
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')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/coze_bot.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('coze_bot')
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'
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', '')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/cron_trigger.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('cron_trigger')
class CronTriggerNode(WorkflowNode):
"""Cron trigger node - triggers workflow on schedule"""
type_name = 'cron_trigger'
category = 'trigger'
icon = 'Timer'
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]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
from datetime import datetime

View File

@@ -5,30 +5,17 @@ Node metadata is loaded from: ../../templates/metadata/nodes/data_transform.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
from ..safe_eval import safe_eval_with_vars
@workflow_node('data_transform')
class DataTransformNode(WorkflowNode):
"""Data transform node - transform data using templates or JSONPath"""
type_name = 'data_transform'
category = 'process'
icon = 'ArrowRightLeft'
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')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/database_query.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('database_query')
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'
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')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/dify_knowledge_quer
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('dify_knowledge_query')
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'
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')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/dify_workflow.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('dify_workflow')
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'
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')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/end.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('end')
class EndNode(WorkflowNode):
"""End node - marks the end of workflow execution"""
type_name = 'end'
category = 'action'
icon = 'PauseCircle'
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]] = []
category = 'control'
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
result = inputs.get('result')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/event_trigger.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('event_trigger')
class EventTriggerNode(WorkflowNode):
"""Event trigger node - triggers workflow on system events"""
type_name = 'event_trigger'
category = 'trigger'
icon = 'Bell'
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]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
from datetime import datetime

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/http_request.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('http_request')
class HTTPRequestNode(WorkflowNode):
"""HTTP request node - make HTTP API calls"""
type_name = 'http_request'
category = 'process'
icon = 'Globe'
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]] = []
config_schema: ClassVar[list[NodeConfig]] = []
category = 'action'
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
import aiohttp

View File

@@ -2,47 +2,16 @@
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('iterator')
class IteratorNode(WorkflowNode):
"""Iterator node - iterate over array items one by one"""
type_name = 'iterator'
category = 'control'
icon = 'Repeat'
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),
]
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'),
]
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': '最大迭代次数'},
),
]
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
items = inputs.get('items', [])

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/knowledge_retrieval
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('knowledge_retrieval')
class KnowledgeRetrievalNode(WorkflowNode):
"""Knowledge retrieval node - search in knowledge base"""
type_name = 'knowledge_retrieval'
category = 'process'
icon = 'Search'
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', '')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/langflow_flow.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('langflow_flow')
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'
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')

View File

@@ -4,81 +4,20 @@ from __future__ import annotations
import logging
import re
from typing import Any, ClassVar
from typing import Any
import langbot_plugin.api.entities.builtin.provider.message as provider_message
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
logger = logging.getLogger(__name__)
@workflow_node('llm_call')
class LLMCallNode(WorkflowNode):
"""LLM call node - invoke large language model"""
type_name = 'llm_call'
category = 'process'
icon = 'Brain'
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),
]
outputs: ClassVar[list[NodePort]] = [
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': '模型'},
),
NodeConfig(
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': '用户提示词模板'},
),
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,
),
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': '最大令牌数'},
),
]
def _resolve_template(self, template: str, inputs: dict[str, Any], context: ExecutionContext) -> str:
"""Resolve {{variable}} placeholders in a template string."""

View File

@@ -2,54 +2,16 @@
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('loop')
class LoopNode(WorkflowNode):
"""Loop node - iterate over items"""
type_name = 'loop'
category = 'control'
icon = 'Repeat'
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),
]
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'),
]
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'],
),
NodeConfig(
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', [])

View File

@@ -9,36 +9,24 @@ The i18n for label and description is handled on the frontend side.
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('mcp_tool')
class MCPToolNode(WorkflowNode):
"""MCP tool node - invoke MCP (Model Context Protocol) tools"""
# Node type for registration
type_name = 'mcp_tool'
# Category and icon - these are not i18n
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'
# Inputs/outputs/config - loaded from YAML at runtime
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]:
"""Execute the MCP tool node

View File

@@ -5,11 +5,10 @@ Node metadata is loaded from: ../../templates/metadata/nodes/memory_store.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
class MemoryHelper:
"""Helper class wrapping context.memory dict with get/set/delete/list_all/append operations"""
@@ -47,24 +46,11 @@ class MemoryHelper:
self.set(key, current, scope=scope, ttl=ttl)
return current
@workflow_node('memory_store')
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'
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')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/merge.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('merge')
class MergeNode(WorkflowNode):
"""Merge node - combine multiple inputs"""
type_name = 'merge'
category = 'control'
icon = 'GitMerge'
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')

View File

@@ -7,29 +7,16 @@ Node metadata (label, description, inputs, outputs, config) is loaded from:
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('message_trigger')
class MessageTriggerNode(WorkflowNode):
"""Message trigger node - triggers workflow on message arrival"""
type_name = 'message_trigger'
category = 'trigger'
icon = 'MessageSquare'
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

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/n8n_workflow.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('n8n_workflow')
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'
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', '')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/opening_statement.y
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('opening_statement')
class OpeningStatementNode(WorkflowNode):
"""Opening statement node - provide conversation opener and suggested questions"""
type_name = 'opening_statement'
category = 'action'
icon = 'MessageSquare'
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', '')

View File

@@ -2,51 +2,16 @@
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('parallel')
class ParallelNode(WorkflowNode):
"""Parallel node - execute multiple branches simultaneously"""
type_name = 'parallel'
category = 'control'
icon = 'Layers'
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),
]
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)'),
]
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': '等待全部完成'},
),
NodeConfig(
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 {

View File

@@ -5,29 +5,17 @@ Node metadata is loaded from: ../../templates/metadata/nodes/parameter_extractor
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('parameter_extractor')
class ParameterExtractorNode(WorkflowNode):
"""Parameter extractor node - extract structured parameters from text"""
type_name = 'parameter_extractor'
category = 'process'
icon: str = 'Variable'
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', [])

View File

@@ -5,11 +5,10 @@
# from __future__ import annotations
# from typing import Any, ClassVar
# from typing import Any
# from ..entities import ExecutionContext
# from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
# from ..node import WorkflowNode, workflow_node
# @workflow_node('plugin_call')
# class PluginCallNode(WorkflowNode):

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/question_classifier
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('question_classifier')
class QuestionClassifierNode(WorkflowNode):
"""Question classifier node - classify user questions into categories"""
type_name = 'question_classifier'
category = 'process'
icon = 'ListFilter'
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', [])

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/redis_operation.yam
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('redis_operation')
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'
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')

View File

@@ -6,31 +6,18 @@ Node metadata is loaded from: ../../templates/metadata/nodes/reply_message.yaml
from __future__ import annotations
import logging
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
logger = logging.getLogger(__name__)
@workflow_node('reply_message')
class ReplyMessageNode(WorkflowNode):
"""Reply message node - reply to the triggering message"""
type_name = 'reply_message'
category = 'action'
icon = 'Send'
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')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/send_message.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('send_message')
class SendMessageNode(WorkflowNode):
"""Send message node - send message to a target"""
type_name = 'send_message'
category = 'action'
icon = 'MessageCircle'
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}'}

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/set_variable.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('set_variable')
class SetVariableNode(WorkflowNode):
"""Set variable node - set workflow or conversation variable"""
type_name = 'set_variable'
category = 'action'
icon = 'Variable'
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')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/store_data.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('store_data')
class StoreDataNode(WorkflowNode):
"""Store data node - save data to storage"""
type_name = 'store_data'
category = 'action'
icon = 'Database'
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', '')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/switch.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('switch')
class SwitchNode(WorkflowNode):
"""Switch node - multi-way branch based on value"""
type_name = 'switch'
category = 'control'
icon = 'Split'
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', '')

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/variable_aggregator
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('variable_aggregator')
class VariableAggregatorNode(WorkflowNode):
"""Variable aggregator node - aggregate variables from multiple branches"""
type_name = 'variable_aggregator'
category = 'control'
icon = 'GitMerge'
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', {})

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/wait.yaml
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('wait')
class WaitNode(WorkflowNode):
"""Wait node - pause execution for a duration"""
type_name = 'wait'
category = 'control'
icon = 'Clock'
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]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
import asyncio

View File

@@ -5,29 +5,16 @@ Node metadata is loaded from: ../../templates/metadata/nodes/webhook_trigger.yam
from __future__ import annotations
from typing import Any, ClassVar
from typing import Any
from ..entities import ExecutionContext
from ..node import WorkflowNode, workflow_node, NodePort, NodeConfig
from ..node import WorkflowNode, workflow_node
@workflow_node('webhook_trigger')
class WebhookTriggerNode(WorkflowNode):
"""Webhook trigger node - triggers workflow via HTTP request"""
type_name = 'webhook_trigger'
category = 'trigger'
icon = 'Webhook'
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]] = []
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
trigger_data = context.trigger_data

View File

@@ -1,22 +1,35 @@
"""Node type registry"""
"""Workflow node type registry."""
from __future__ import annotations
import copy
import logging
from typing import Any, Optional
from .node import WorkflowNode, get_pending_registrations, clear_pending_registrations
from .metadata import build_node_type
from .node import WorkflowNode, clear_pending_registrations, get_pending_registrations
logger = logging.getLogger(__name__)
class NodeConflictError(Exception):
"""Raised when two workflow node metadata definitions conflict."""
class NodeTypeRegistry:
"""
Central registry for all workflow node types.
Supports both built-in and plugin-provided nodes.
Central registry for workflow node types.
YAML metadata is the UI-facing source of truth. Python node classes are
registered separately and provide execution logic only.
"""
_instance: Optional['NodeTypeRegistry'] = None
def __init__(self):
self._nodes: dict[str, type[WorkflowNode]] = {}
self._metadata: dict[str, dict[str, Any]] = {}
self._metadata_sources: dict[str, str] = {}
self._categories: dict[str, list[str]] = {
'trigger': [],
'process': [],
@@ -25,144 +38,396 @@ class NodeTypeRegistry:
'integration': [],
'misc': [],
}
self._conflicts: list[dict[str, str]] = []
@classmethod
def instance(cls) -> 'NodeTypeRegistry':
"""Get singleton instance"""
"""Get singleton instance."""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def register_metadata(self, metadata: dict[str, Any], source: str = 'core') -> bool:
"""Register YAML metadata for a workflow node type.
Core metadata cannot be overridden by plugin metadata. Plugin-plugin
conflicts are allowed with a warning so hot-reload/development flows can
replace plugin definitions.
"""
node_type = build_node_type(metadata)
existing_source = self._metadata_sources.get(node_type)
if existing_source:
conflict = {'type': node_type, 'existing_source': existing_source, 'new_source': source}
if existing_source == 'core' and source != 'core':
self._conflicts.append(conflict)
logger.error('Plugin source %s attempted to override core workflow node %s', source, node_type)
return False
logger.warning(
'Workflow node metadata %s from %s overrides previous source %s', node_type, source, existing_source
)
cached_metadata = copy.deepcopy(metadata)
cached_metadata['_source'] = source
self._metadata[node_type] = cached_metadata
self._metadata_sources[node_type] = source
self._add_to_category(metadata.get('category', 'misc'), node_type)
return True
def register(self, node_type: str, node_class: type[WorkflowNode]):
"""
Register a node type.
"""Register a Python workflow node implementation class."""
canonical_type = self._canonical_type_for_class(node_type, node_class)
self._nodes[canonical_type] = node_class
Args:
node_type: Unique type identifier
node_class: WorkflowNode subclass
"""
self._nodes[node_type] = node_class
metadata = self.get_metadata(canonical_type)
if metadata:
category = metadata.get('category', getattr(node_class, 'category', 'misc'))
else:
category = getattr(node_class, 'category', 'misc')
logger.warning('Workflow node implementation %s has no YAML metadata', canonical_type)
# Add to category
category = getattr(node_class, 'category', 'misc')
if category not in self._categories:
self._categories[category] = []
if node_type not in self._categories[category]:
self._categories[category].append(node_type)
self._add_to_category(category, canonical_type)
def unregister(self, node_type: str):
"""Unregister a node type"""
if node_type in self._nodes:
node_class = self._nodes[node_type]
category = getattr(node_class, 'category', 'misc')
if category in self._categories and node_type in self._categories[category]:
self._categories[category].remove(node_type)
del self._nodes[node_type]
"""Unregister a Python workflow node implementation."""
canonical_type = self._resolve_registered_node_key(node_type)
if canonical_type is None:
return
node_class = self._nodes[canonical_type]
metadata = self.get_metadata(canonical_type)
category = metadata.get('category') if metadata else getattr(node_class, 'category', 'misc')
self._remove_from_category(category or 'misc', canonical_type)
del self._nodes[canonical_type]
def unregister_metadata(self, node_type: str):
"""Unregister YAML metadata for a node type, primarily for plugin unload."""
canonical_type = self._resolve_metadata_key(node_type)
if canonical_type is None:
return
metadata = self._metadata[canonical_type]
self._remove_from_category(metadata.get('category', 'misc'), canonical_type)
del self._metadata[canonical_type]
self._metadata_sources.pop(canonical_type, None)
def get(self, node_type: str) -> Optional[type[WorkflowNode]]:
"""Get node class by type. Supports both 'category.type_name' and short 'type_name' formats."""
# First try exact match (category.type_name format)
if node_type in self._nodes:
return self._nodes[node_type]
"""Get node class by type. Supports both ``category.name`` and short names."""
canonical_type = self._resolve_registered_node_key(node_type)
if canonical_type:
return self._nodes[canonical_type]
# Try short name format (e.g., 'dify_workflow' -> 'integration.dify_workflow')
# Search through all registered nodes for a matching type_name
for registered_type, node_class in self._nodes.items():
if node_class.type_name == node_type:
return node_class
# Lazy-process pending registrations so execution paths that didn't
# explicitly warm the registry can still resolve newly imported nodes.
if get_pending_registrations():
self.process_pending_registrations()
canonical_type = self._resolve_registered_node_key(node_type)
if canonical_type:
return self._nodes[canonical_type]
if node_type in self._nodes:
return self._nodes[node_type]
for registered_type, node_class in self._nodes.items():
if node_class.type_name == node_type:
return node_class
return None
def get_metadata(self, node_type: str) -> Optional[dict[str, Any]]:
"""Get YAML metadata by full type or short node name."""
canonical_type = self._resolve_metadata_key(node_type)
if canonical_type:
return copy.deepcopy(self._metadata[canonical_type])
return None
def create_instance(
self, node_type: str, node_id: str, config: dict[str, Any], ap: Optional['app.Application'] = None
) -> Optional[WorkflowNode]:
"""Create a node instance. Supports both 'category.type_name' and short 'type_name' formats."""
"""Create a node instance. Supports both ``category.name`` and short names."""
node_class = self.get(node_type)
if node_class:
return node_class(node_id, config, ap=ap)
logger.warning('No workflow node implementation registered for type: %s', node_type)
return None
def get_merged_schema(self, node_type: str) -> Optional[dict[str, Any]]:
"""Get frontend schema from YAML metadata.
Python node classes no longer carry UI metadata. If a node class is
registered but has no YAML metadata, a minimal schema is generated
from the class attributes (category, type_name) so it still appears
in the editor.
"""
metadata = self.get_metadata(node_type)
node_class = self.get(node_type)
if metadata:
schema = self._metadata_to_schema(metadata)
if node_class:
# Supplement pipeline config reuse fields from Python class
for key in ('config_schema_source', 'config_stages'):
if not schema.get(key) and getattr(node_class, key, None):
schema[key] = getattr(node_class, key)
return schema
if node_class:
# Fallback: node has Python class but no YAML metadata
short_name = getattr(node_class, 'type_name', '') or node_type.split('.')[-1]
category = getattr(node_class, 'category', 'misc')
return {
'type': f'{category}.{short_name}',
'name': short_name,
'label': self._normalize_i18n(None, self._prettify_name(short_name)),
'description': self._normalize_i18n(None, ''),
'category': category,
'icon': '',
'color': '',
'inputs': [],
'outputs': [],
'config_schema': [],
'config_schema_source': getattr(node_class, 'config_schema_source', None),
'config_stages': getattr(node_class, 'config_stages', []),
'source': 'python-only',
}
return None
def list_all(self) -> list[dict[str, Any]]:
"""
Get all registered node types as schema list.
Returns:
List of node schemas
"""
return [node_class.to_schema() for node_class in self._nodes.values()]
"""Get all registered node type schemas, including metadata-only nodes."""
node_types = self._ordered_node_types(set(self._metadata.keys()) | set(self._nodes.keys()))
return [schema for node_type in node_types if (schema := self.get_merged_schema(node_type)) is not None]
def list_by_category(self, category: str) -> list[dict[str, Any]]:
"""
Get node types by category.
Args:
category: Category name (trigger, process, control, action, integration, misc)
Returns:
List of node schemas in the category
"""
"""Get node type schemas by category."""
if category not in self._categories:
return []
return [
self._nodes[node_type].to_schema() for node_type in self._categories[category] if node_type in self._nodes
]
return [schema for node_type in self._categories[category] if (schema := self.get_merged_schema(node_type)) is not None]
def get_categories(self) -> dict[str, list[dict[str, Any]]]:
"""
Get all nodes organized by category.
Returns:
Dictionary mapping category names to lists of node schemas
"""
"""Get all nodes organized by category."""
return {category: self.list_by_category(category) for category in self._categories.keys()}
def has_type(self, node_type: str) -> bool:
"""Check if a node type is registered. Supports both formats."""
return self.get(node_type) is not None
"""Check whether a node has metadata or an implementation registered."""
return self.get_metadata(node_type) is not None or self.get(node_type) is not None
def process_pending_registrations(self):
"""Process all pending node registrations from decorators"""
"""Process all pending node registrations from decorators."""
for node_type, node_class in get_pending_registrations():
# Use category.type_name format for consistency with frontend
category = getattr(node_class, 'category', 'misc')
full_type = f'{category}.{node_type}'
self.register(full_type, node_class)
self.register(node_type, node_class)
clear_pending_registrations()
def count(self) -> int:
"""Get total number of registered node types"""
return len(self._nodes)
"""Get total number of node types exposed by metadata or implementation."""
return len(set(self._metadata.keys()) | set(self._nodes.keys()))
def metadata_count(self) -> int:
"""Get number of registered YAML metadata definitions."""
return len(self._metadata)
def get_conflicts(self) -> list[dict[str, str]]:
"""Return metadata registration conflicts."""
return copy.deepcopy(self._conflicts)
def clear(self):
"""Clear all registrations (for testing)"""
"""Clear all registrations (for testing)."""
self._nodes.clear()
self._metadata.clear()
self._metadata_sources.clear()
self._conflicts.clear()
for category in self._categories:
self._categories[category] = []
def _canonical_type_for_class(self, node_type: str, node_class: type[WorkflowNode]) -> str:
short_name = node_type.split('.')[-1]
metadata_key = self._resolve_metadata_key(node_type) or self._resolve_metadata_key(short_name)
if metadata_key:
return metadata_key
category = getattr(node_class, 'category', 'misc')
return node_type if '.' in node_type else f'{category}.{short_name}'
def _resolve_registered_node_key(self, node_type: str) -> Optional[str]:
if node_type in self._nodes:
return node_type
short_name = node_type.split('.')[-1]
for registered_type, node_class in self._nodes.items():
if registered_type.split('.')[-1] == short_name or getattr(node_class, 'type_name', None) == short_name:
return registered_type
return None
def _resolve_metadata_key(self, node_type: str) -> Optional[str]:
if node_type in self._metadata:
return node_type
short_name = node_type.split('.')[-1]
for registered_type, metadata in self._metadata.items():
if registered_type.split('.')[-1] == short_name or metadata.get('name') == short_name:
return registered_type
return None
def _ordered_node_types(self, node_types: set[str]) -> list[str]:
ordered: list[str] = []
for category in self._categories:
for node_type in self._categories[category]:
if node_type in node_types and node_type not in ordered:
ordered.append(node_type)
for node_type in sorted(node_types):
if node_type not in ordered:
ordered.append(node_type)
return ordered
def _add_to_category(self, category: str, node_type: str) -> None:
if category not in self._categories:
self._categories[category] = []
if node_type not in self._categories[category]:
self._categories[category].append(node_type)
def _remove_from_category(self, category: str, node_type: str) -> None:
if category in self._categories and node_type in self._categories[category]:
self._categories[category].remove(node_type)
def _metadata_to_schema(self, metadata: dict[str, Any]) -> dict[str, Any]:
node_type = build_node_type(metadata)
node_name = metadata.get('name', node_type.split('.')[-1])
return {
'type': node_type,
'name': node_name,
'label': self._normalize_i18n(metadata.get('label'), self._prettify_name(node_name)),
'description': self._normalize_i18n(metadata.get('description'), ''),
'category': metadata.get('category', 'misc'),
'icon': metadata.get('icon', ''),
'color': metadata.get('color', ''),
'inputs': [self._normalize_port_item(item) for item in metadata.get('inputs', [])],
'outputs': [self._normalize_port_item(item) for item in metadata.get('outputs', [])],
'config_schema': [self._normalize_config_item(item) for item in metadata.get('config', [])],
'config_schema_source': metadata.get('config_schema_source'),
'config_stages': metadata.get('config_stages', []),
'source': metadata.get('_source', 'core'),
}
def _merge_missing_schema_fields(self, yaml_schema: dict[str, Any], python_schema: dict[str, Any]) -> dict[str, Any]:
result = copy.deepcopy(yaml_schema)
for key in ('config_schema_source', 'config_stages'):
if not result.get(key) and python_schema.get(key):
result[key] = python_schema[key]
return result
def _normalize_port_item(self, port: dict[str, Any]) -> dict[str, Any]:
item = copy.deepcopy(port)
name = item.get('name', '')
item['label'] = self._normalize_i18n(item.get('label'), self._prettify_name(name))
item['description'] = self._normalize_i18n(item.get('description'), '')
item.setdefault('type', 'any')
item.setdefault('required', True)
return item
def _normalize_config_item(self, config: dict[str, Any]) -> dict[str, Any]:
item = copy.deepcopy(config)
name = item.get('name', '')
frontend_type = self._normalize_config_type(item.get('type', 'string'))
item['id'] = item.get('id') or name
item['type'] = frontend_type
item['label'] = self._normalize_i18n(item.get('label'), self._prettify_name(name))
item['description'] = self._normalize_i18n(item.get('description'), '')
item['required'] = bool(item.get('required', False))
item['default'] = item.get('default', self._default_value_for_type(frontend_type))
if 'options' in item:
item['options'] = self._normalize_options(item.get('options'), name)
return item
def _normalize_options(self, options: Any, field_name: str) -> list[dict[str, Any]]:
if not isinstance(options, list):
return []
normalized: list[dict[str, Any]] = []
for option in options:
if isinstance(option, dict):
option_item = copy.deepcopy(option)
option_name = option_item.get('name', option_item.get('value', ''))
option_item['name'] = str(option_name)
option_item['label'] = self._normalize_i18n(option_item.get('label'), str(option_name))
normalized.append(option_item)
else:
option_name = str(option)
normalized.append({'name': option_name, 'label': self._normalize_i18n(None, option_name)})
return normalized
def _normalize_i18n(self, value: Any, fallback: str) -> dict[str, str]:
if isinstance(value, dict):
en_value = (
value.get('en_US')
or value.get('en-US')
or value.get('en')
or value.get('en_US'.replace('_', '-'))
or fallback
)
zh_value = value.get('zh_Hans') or value.get('zh-Hans') or value.get('zh-CN') or value.get('zh') or en_value
return {
'en_US': str(en_value),
'en': str(en_value),
'en-US': str(en_value),
'zh_Hans': str(zh_value),
'zh-Hans': str(zh_value),
'zh-CN': str(zh_value),
}
if isinstance(value, str) and value:
return {
'en_US': value,
'en': value,
'en-US': value,
'zh_Hans': value,
'zh-Hans': value,
'zh-CN': value,
}
return {
'en_US': fallback,
'en': fallback,
'en-US': fallback,
'zh_Hans': fallback,
'zh-Hans': fallback,
'zh-CN': fallback,
}
def _normalize_config_type(self, field_type: str) -> str:
type_map = {
'number': 'float',
'json': 'text',
'textarea': 'text',
}
return type_map.get(field_type, field_type)
def _default_value_for_type(self, field_type: str) -> Any:
if field_type == 'boolean':
return False
if field_type in {'integer', 'float'}:
return 0
if field_type in {'array[string]', 'knowledge-base-multi-selector', 'tools-selector'}:
return []
if field_type == 'model-fallback-selector':
return {'primary': '', 'fallbacks': []}
if field_type == 'prompt-editor':
return [{'role': 'system', 'content': ''}]
return ''
def _prettify_name(self, name: str) -> str:
return ' '.join(part.capitalize() for part in str(name).replace('-', '_').split('_') if part)
# Convenience functions for module-level access
def register_node(node_type: str, node_class: type[WorkflowNode]):
"""Register a node type to the global registry"""
"""Register a node type to the global registry."""
NodeTypeRegistry.instance().register(node_type, node_class)
def get_node_class(node_type: str) -> Optional[type[WorkflowNode]]:
"""Get a node class from the global registry"""
"""Get a node class from the global registry."""
return NodeTypeRegistry.instance().get(node_type)
def list_node_types() -> list[dict[str, Any]]:
"""List all registered node types"""
"""List all registered node types."""
return NodeTypeRegistry.instance().list_all()

View File

@@ -1,9 +1,14 @@
# Call Pipeline Node Configuration
name: call_pipeline
label:
en_US: Call Pipeline
zh_Hans: 调用 Pipeline
category: action
icon: "⚙️"
icon: Workflow
color: '#ef4444'
description: 'workflows.nodes.callPipelineDescription'
description:
en_US: Invoke an existing Pipeline for processing
zh_Hans: 调用现有的 Pipeline 进行处理
inputs:
- name: query

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/code_executor.py
name: code_executor
label:
en_US: Code Executor
zh_Hans: 代码执行
category: process
icon: "💻"
icon: Code
color: '#3b82f6'
description: 'workflows.nodes.codeExecutorDescription'
description:
en_US: Execute custom code to process data
zh_Hans: 执行自定义代码处理数据
inputs:
- name: input

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/condition.py
name: condition
label:
en_US: Condition
zh_Hans: 条件分支
category: control
icon: "🔀"
icon: GitBranch
color: '#8b5cf6'
description: 'workflows.nodes.conditionDescription'
description:
en_US: Branch workflow based on a condition
zh_Hans: 根据条件分支工作流
inputs:
- name: input

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/cron_trigger.py
name: cron_trigger
label:
en_US: Scheduled Trigger
zh_Hans: 定时触发
category: trigger
icon: "⏰"
icon: Timer
color: '#22c55e'
description: 'workflows.nodes.cronTriggerDescription'
description:
en_US: Trigger workflow on a scheduled time
zh_Hans: 按定时计划触发工作流
inputs: []

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/data_transform.py
name: data_transform
label:
en_US: Data Transform
zh_Hans: 数据转换
category: process
icon: "🔄"
icon: ArrowRightLeft
color: '#3b82f6'
description: 'workflows.nodes.dataTransformDescription'
description:
en_US: Transform data using templates or JSONPath
zh_Hans: 使用模板或 JSONPath 转换数据
inputs:
- name: data

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/database_query.py
name: database_query
label:
en_US: Database Query
zh_Hans: 数据库查询
category: integration
icon: "🗄️"
icon: Database
color: '#ec4899'
description: 'workflows.nodes.databaseQueryDescription'
description:
en_US: Execute database queries
zh_Hans: 执行数据库查询
inputs:
- name: parameters

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/end.py
name: end
label:
en_US: End
zh_Hans: 结束
category: control
icon: "🛑"
icon: PauseCircle
color: '#8b5cf6'
description: 'workflows.nodes.endDescription'
description:
en_US: End the workflow execution
zh_Hans: 结束工作流执行
inputs:
- name: input

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/event_trigger.py
name: event_trigger
label:
en_US: Event Trigger
zh_Hans: 事件触发
category: trigger
icon: "📡"
icon: Bell
color: '#22c55e'
description: 'workflows.nodes.eventTriggerDescription'
description:
en_US: Trigger workflow when a system event occurs
zh_Hans: 当系统事件发生时触发工作流
inputs: []

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/http_request.py
name: http_request
label:
en_US: HTTP Request
zh_Hans: HTTP 请求
category: action
icon: "🌍"
icon: Globe
color: '#10b981'
description: 'workflows.nodes.httpRequestDescription'
description:
en_US: Make HTTP requests to external APIs
zh_Hans: 向外部 API 发送 HTTP 请求
inputs:
- name: body

View File

@@ -1,9 +1,14 @@
# Iterator Node Configuration
name: iterator
label:
en_US: Iterator
zh_Hans: 迭代器
category: control
icon: "🔄"
icon: Repeat
color: '#f59e0b'
description: 'workflows.nodes.iteratorDescription'
description:
en_US: Iterate over array elements one by one
zh_Hans: 逐个遍历数组元素
inputs:
- name: array

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/knowledge_retrieval.py
name: knowledge_retrieval
label:
en_US: Knowledge Retrieval
zh_Hans: 知识库检索
category: process
icon: "📚"
icon: Search
color: '#8b5cf6'
description: 'workflows.nodes.knowledgeRetrievalDescription'
description:
en_US: Retrieve relevant information from knowledge bases
zh_Hans: 从知识库中检索相关信息
inputs:
- name: query
@@ -46,7 +51,7 @@ outputs:
config:
- name: knowledge_bases
type: json
type: knowledge-base-multi-selector
required: true
default: []
label:
@@ -101,7 +106,7 @@ config:
zh_Hans: 使用重排序模型提高结果相关性
- name: rerank_model
type: string
type: rerank-model-selector
default: ""
label:
en_US: Rerank Model
@@ -109,3 +114,5 @@ config:
description:
en_US: Model to use for reranking results
zh_Hans: 用于结果重排序的模型
show_if:
rerank_enabled: true

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/llm_call.py
name: llm_call
label:
en_US: LLM Call
zh_Hans: LLM 调用
category: process
icon: "🤖"
icon: Brain
color: '#8b5cf6'
description: 'workflows.nodes.llmCallDescription'
description:
en_US: Call a large language model to generate responses
zh_Hans: 调用大语言模型生成响应
inputs:
- name: input

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/loop.py
name: loop
label:
en_US: Loop
zh_Hans: 循环
category: control
icon: "🔁"
icon: Repeat
color: '#8b5cf6'
description: 'workflows.nodes.loopDescription'
description:
en_US: Iterate over items or repeat until condition
zh_Hans: 遍历项目或重复直到满足条件
inputs:
- name: items

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/mcp_tool.py
name: mcp_tool
label:
en_US: MCP Tool
zh_Hans: MCP 工具
category: integration
icon: 🔧
icon: Wrench
color: '#ec4899'
description: workflows.nodes.mcpToolDescription
description:
en_US: Invoke an MCP (Model Context Protocol) tool
zh_Hans: 调用 MCP 工具
inputs:
- name: arguments
type: object

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/memory_store.py
name: memory_store
label:
en_US: Memory Store
zh_Hans: 记忆存储
category: integration
icon: "💾"
icon: HardDrive
color: '#ec4899'
description: 'workflows.nodes.memoryStoreDescription'
description:
en_US: Store and retrieve data from workflow memory
zh_Hans: 从工作流记忆中存储和检索数据
inputs:
- name: value

View File

@@ -1,9 +1,14 @@
# Merge Node Configuration
name: merge
label:
en_US: Merge
zh_Hans: 合并
category: control
icon: "🔗"
icon: GitMerge
color: '#f59e0b'
description: 'workflows.nodes.mergeDescription'
description:
en_US: Merge multiple branches back together
zh_Hans: 将多个分支合并在一起
inputs:
- name: input_1

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/message_trigger.py
name: message_trigger
label:
en_US: Message Trigger
zh_Hans: 消息触发
category: trigger
icon: "💬"
icon: MessageSquare
color: '#22c55e'
description: 'workflows.nodes.messageTriggerDescription'
description:
en_US: Trigger workflow when a message is received
zh_Hans: 当收到消息时触发工作流
inputs: []

View File

@@ -1,9 +1,14 @@
# Opening Statement Node Configuration
name: opening_statement
label:
en_US: Opening Statement
zh_Hans: 对话开场白
category: action
icon: "👋"
icon: MessageSquare
color: '#ef4444'
description: 'workflows.nodes.openingStatementDescription'
description:
en_US: Provide conversation opener and suggested questions
zh_Hans: 提供对话开场白和建议问题
inputs: []

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/parallel.py
name: parallel
label:
en_US: Parallel
zh_Hans: 并行执行
category: control
icon: "⚡"
icon: Layers
color: '#8b5cf6'
description: 'workflows.nodes.parallelDescription'
description:
en_US: Execute multiple branches in parallel
zh_Hans: 并行执行多个分支
inputs:
- name: input

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/parameter_extractor.py
name: parameter_extractor
label:
en_US: Parameter Extractor
zh_Hans: 参数提取器
category: process
icon: "🔍"
icon: Variable
color: '#8b5cf6'
description: 'workflows.nodes.parameterExtractorDescription'
description:
en_US: Extract structured parameters from text using AI
zh_Hans: 使用 AI 从文本中提取结构化参数
inputs:
- name: text

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/question_classifier.py
name: question_classifier
label:
en_US: Question Classifier
zh_Hans: 问题分类器
category: process
icon: "🏷️"
icon: ListFilter
color: '#8b5cf6'
description: 'workflows.nodes.questionClassifierDescription'
description:
en_US: Classify questions into predefined categories using AI
zh_Hans: 使用 AI 将问题分类到预定义类别
inputs:
- name: question

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/redis_operation.py
name: redis_operation
label:
en_US: Redis Operation
zh_Hans: Redis 操作
category: integration
icon: "🔴"
icon: Server
color: '#ec4899'
description: 'workflows.nodes.redisOperationDescription'
description:
en_US: Perform Redis cache operations
zh_Hans: 执行 Redis 缓存操作
inputs:
- name: key

View File

@@ -1,9 +1,14 @@
# Reply Message Node Configuration
name: reply_message
label:
en_US: Reply Message
zh_Hans: 回复消息
category: action
icon: "↩️"
icon: Send
color: '#ef4444'
description: 'workflows.nodes.replyMessageDescription'
description:
en_US: Reply to the message that triggered the workflow
zh_Hans: 回复触发工作流的消息
inputs:
- name: message

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/send_message.py
name: send_message
label:
en_US: Send Message
zh_Hans: 发送消息
category: action
icon: "📤"
icon: MessageCircle
color: '#10b981'
description: 'workflows.nodes.sendMessageDescription'
description:
en_US: Send a message to a chat or user
zh_Hans: 向聊天或用户发送消息
inputs:
- name: content

View File

@@ -1,9 +1,14 @@
# Set Variable Node Configuration
name: set_variable
label:
en_US: Set Variable
zh_Hans: 设置变量
category: action
icon: "📝"
icon: Variable
color: '#ef4444'
description: 'workflows.nodes.setVariableDescription'
description:
en_US: Set a context variable value
zh_Hans: 设置上下文变量值
inputs:
- name: value

View File

@@ -1,9 +1,14 @@
# Store Data Node Configuration
name: store_data
label:
en_US: Store Data
zh_Hans: 存储数据
category: action
icon: "💾"
icon: Database
color: '#ef4444'
description: 'workflows.nodes.storeDataDescription'
description:
en_US: Store data to persistent storage
zh_Hans: 将数据存储到持久化存储
inputs:
- name: key

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/switch.py
name: switch
label:
en_US: Switch
zh_Hans: 多路分支
category: control
icon: "🔀"
icon: Split
color: '#8b5cf6'
description: 'workflows.nodes.switchDescription'
description:
en_US: Branch workflow based on multiple cases
zh_Hans: 根据多个条件分支工作流
inputs:
- name: input

View File

@@ -1,9 +1,14 @@
# Variable Aggregator Node Configuration
name: variable_aggregator
label:
en_US: Variable Aggregator
zh_Hans: 变量聚合器
category: control
icon: "📊"
icon: GitMerge
color: '#f59e0b'
description: 'workflows.nodes.variableAggregatorDescription'
description:
en_US: Aggregate variable outputs from multiple branches
zh_Hans: 聚合多个分支的变量输出
inputs:
- name: variables

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/wait.py
name: wait
label:
en_US: Wait
zh_Hans: 等待
category: control
icon: "⏳"
icon: Clock
color: '#8b5cf6'
description: 'workflows.nodes.waitDescription'
description:
en_US: Pause workflow execution for a specified duration
zh_Hans: 暂停工作流执行指定时间
inputs:
- name: input

View File

@@ -3,10 +3,15 @@
# The corresponding Python implementation is in: pkg/workflow/nodes/webhook_trigger.py
name: webhook_trigger
label:
en_US: Webhook Trigger
zh_Hans: Webhook 触发
category: trigger
icon: "🌐"
icon: Webhook
color: '#22c55e'
description: 'workflows.nodes.webhookTriggerDescription'
description:
en_US: Trigger workflow via HTTP webhook
zh_Hans: 通过 HTTP 请求触发工作流
inputs: []

View File

@@ -79,19 +79,17 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
return () => setDetailEntityName(null);
}, [id, isCreateMode, workflows, setDetailEntityName, t]);
// Load node types
// Load node types - always fetch from backend to ensure fresh metadata
useEffect(() => {
if (nodeTypes.length === 0) {
backendClient
.getWorkflowNodeTypes()
.then((resp) => {
setNodeTypes(resp.node_types, resp.categories);
})
.catch((err) => {
console.error('Failed to load node types:', err);
});
}
}, [nodeTypes.length, setNodeTypes]);
backendClient
.getWorkflowNodeTypes()
.then((resp) => {
setNodeTypes(resp.node_types, resp.categories);
})
.catch((err) => {
console.error('Failed to load node types:', err);
});
}, [setNodeTypes]);
// Load workflow data
useEffect(() => {

View File

@@ -14,8 +14,6 @@ import {
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
NODE_ICONS,
NODE_TYPE_I18N_KEYS,
CATEGORY_I18N_KEYS,
PALETTE_CATEGORY_COLORS as categoryColors,
PALETTE_CATEGORY_BG as categoryBgColors,
@@ -26,9 +24,6 @@ import {
} from './workflow-constants';
import { resolveI18nLabel } from './workflow-i18n';
// Use shared icon mapping
const nodeIcons = NODE_ICONS;
// Use shared category i18n keys
const categoryI18nKeys = CATEGORY_I18N_KEYS;
@@ -44,16 +39,6 @@ interface NodeTypeForUI {
description?: Record<string, string>;
}
// Default node types generated from shared constants
const defaultNodeTypes: NodeTypeForUI[] = Object.entries(
NODE_TYPE_I18N_KEYS,
).map(([type, keys]) => ({
type,
category: type.split('.')[0],
labelKey: keys.labelKey,
descriptionKey: keys.descriptionKey,
}));
export default function NodePalette() {
const { t, i18n } = useTranslation();
const { nodeTypes: backendNodeTypes, nodeCategories } = useWorkflowStore();
@@ -96,24 +81,25 @@ export default function NodePalette() {
[t],
);
// Use backend node types if available, otherwise use defaults
// Backend node types are the only source of palette node definitions.
const nodeTypes = useMemo((): NodeTypeForUI[] => {
if (backendNodeTypes && backendNodeTypes.length > 0) {
return backendNodeTypes.map((node) => {
const i18nKeys = findNodeI18nKeys(node.type);
return {
type: node.type,
category: node.category,
labelKey: i18nKeys?.labelKey,
descriptionKey: i18nKeys?.descriptionKey,
// Keep raw label dict as fallback for unknown nodes
label: i18nKeys ? undefined : node.label,
description: i18nKeys ? undefined : node.description,
};
});
if (!backendNodeTypes || backendNodeTypes.length === 0) {
return [];
}
return defaultNodeTypes;
return backendNodeTypes.map((node) => {
const i18nKeys = findNodeI18nKeys(node.type);
return {
type: node.type,
category: node.category,
icon: node.icon,
labelKey: i18nKeys?.labelKey,
descriptionKey: i18nKeys?.descriptionKey,
label: i18nKeys ? undefined : node.label,
description: i18nKeys ? undefined : node.description,
};
});
}, [backendNodeTypes]);
// Filter nodes based on search query

View File

@@ -20,7 +20,6 @@ import {
} from 'lucide-react';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
import { getNodeConfig } from './node-configs';
import i18n from 'i18next';
import { I18nObject } from '@/app/infra/entities/common';
import { normalizeWorkflowNodeTypeMeta } from './workflow-node-metadata';
@@ -232,58 +231,38 @@ export default function PropertyPanel({
}, [edges, selectedEdgeId]);
// Get node type metadata for selected node
// Priority: API metadata first, local registry as normalized fallback
// Supports both full type (category.name) and short name matching
const nodeTypeMeta = useMemo(() => {
if (!selectedNode) return null;
const nodeType = selectedNode.data.type;
return normalizeWorkflowNodeTypeMeta(
nodeType,
nodeTypes.find((t) => t.type === nodeType),
);
const shortName = nodeType.includes('.') ? nodeType.split('.').pop()! : nodeType;
const matched = nodeTypes.find((t) => {
if (t.type === nodeType) return true;
const tShort = t.type.includes('.') ? t.type.split('.').pop()! : t.type;
return tShort === shortName;
});
return normalizeWorkflowNodeTypeMeta(nodeType, matched);
}, [selectedNode, nodeTypes]);
// Get local node config for additional metadata not carried by backend schema
const localNodeConfig = useMemo(() => {
if (!selectedNode) return null;
const nodeType = selectedNode.data.type;
return getNodeConfig(nodeType) || null;
}, [selectedNode]);
// Prefer local registry config schema so workflow editor can reuse the existing
// form item definitions, i18n labels/descriptions and option labels consistently.
// Fall back to backend metadata for nodes that do not exist in the local registry.
// Backend YAML is the single source of truth for node configuration schema.
const configSchema = useMemo(() => {
const localConfigSchema =
(localNodeConfig?.configSchema as IDynamicFormItemSchema[]) || [];
const backendConfigSchema =
(nodeTypeMeta?.config_schema as IDynamicFormItemSchema[]) || [];
const rawConfigSchema =
localConfigSchema.length > 0 ? localConfigSchema : backendConfigSchema;
return rawConfigSchema.map((item) => {
const backendItem = backendConfigSchema.find(
(candidate) => candidate.name === item.name || candidate.id === item.id,
);
return {
...(backendItem || {}),
...item,
label: item.label ||
backendItem?.label || {
en_US: item.name,
zh_Hans: item.name,
},
description: item.description ||
backendItem?.description || {
en_US: '',
zh_Hans: '',
},
options: item.options || backendItem?.options,
show_if: item.show_if || backendItem?.show_if,
};
});
}, [localNodeConfig?.configSchema, nodeTypeMeta?.config_schema]);
return backendConfigSchema.map((item) => ({
...item,
id: item.id || item.name,
label: item.label || {
en_US: item.name,
zh_Hans: item.name,
},
description: item.description || {
en_US: '',
zh_Hans: '',
},
}));
}, [nodeTypeMeta?.config_schema]);
// Get available input variables from connected upstream nodes
const availableInputVariables = useMemo(() => {
@@ -555,9 +534,6 @@ export default function PropertyPanel({
? extractI18nLabel(nodeTypeMeta.description)
: undefined;
// Get node category color from local config
const nodeColor = localNodeConfig?.color || nodeTypeMeta?.color;
return (
<TooltipProvider>
<div className="h-full w-full flex flex-col overflow-hidden">

View File

@@ -6,5 +6,3 @@ export {
export { default as NodePalette } from './NodePalette';
export { default as PropertyPanel } from './PropertyPanel';
// Export node configurations
export * from './node-configs';

View File

@@ -1,774 +0,0 @@
/**
* AI Node Configurations
*
* Defines configurations for all AI-related node types:
* - llm_call: Call a large language model
* - question_classifier: Classify user questions into categories
* - parameter_extractor: Extract structured parameters from text
* - knowledge_retrieval: Retrieve information from knowledge bases
* - text_embedding: Generate text embeddings
* - intent_recognition: Recognize user intent
*/
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
import { NodeConfigMeta, createInput, createOutput } from './types';
/**
* LLM Call Node
* Makes a call to a large language model
*/
export const llmCallConfig: NodeConfigMeta = {
nodeType: 'llm_call',
label: {
en_US: 'LLM Call',
zh_Hans: 'LLM 调用',
},
description: {
en_US: 'Call a large language model to generate responses',
zh_Hans: '调用大语言模型生成响应',
},
icon: 'Brain',
category: 'process',
color: '#8b5cf6',
inputs: [
createInput('input', 'string', {
description: 'Input text to send to the model',
label: { en_US: 'Input', zh_Hans: '输入' },
}),
createInput('context', 'object', {
description: 'Additional context data',
label: { en_US: 'Context', zh_Hans: '上下文' },
required: false,
}),
],
outputs: [
createOutput('response', 'string', {
description: 'Model response text',
label: { en_US: 'Response', zh_Hans: '响应' },
}),
createOutput('usage', 'object', {
description: 'Token usage information',
label: { en_US: 'Usage', zh_Hans: '使用量' },
}),
createOutput('parsed', 'object', {
description: 'Parsed output (if output format is JSON)',
label: { en_US: 'Parsed', zh_Hans: '解析结果' },
}),
],
configSchema: [
{
id: 'model',
name: 'model',
type: DynamicFormItemType.LLM_MODEL_SELECTOR,
label: {
en_US: 'Model',
zh_Hans: '模型',
},
description: {
en_US: 'Select the LLM model to use',
zh_Hans: '选择要使用的 LLM 模型',
},
required: true,
default: '',
},
{
id: 'system_prompt',
name: 'system_prompt',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'System Prompt',
zh_Hans: '系统提示词',
},
description: {
en_US:
'System prompt to set the model behavior (supports variable interpolation with {{variable}})',
zh_Hans:
'设置模型行为的系统提示词(支持使用 {{variable}} 进行变量插值)',
},
required: false,
default: '',
},
{
id: 'user_prompt_template',
name: 'user_prompt_template',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'User Prompt Template',
zh_Hans: '用户提示词模板',
},
description: {
en_US:
'User prompt template with variable placeholders (e.g., {{input}}, {{context.key}})',
zh_Hans:
'带有变量占位符的用户提示词模板(例如 {{input}}、{{context.key}}',
},
required: true,
default: '{{input}}',
},
{
id: 'temperature',
name: 'temperature',
type: DynamicFormItemType.FLOAT,
label: {
en_US: 'Temperature',
zh_Hans: '温度',
},
description: {
en_US:
'Controls randomness in responses (0.0 = deterministic, 2.0 = very random)',
zh_Hans: '控制响应的随机性0.0 = 确定性2.0 = 非常随机)',
},
required: false,
default: 0.7,
},
{
id: 'max_tokens',
name: 'max_tokens',
type: DynamicFormItemType.INT,
label: {
en_US: 'Max Tokens',
zh_Hans: '最大令牌数',
},
description: {
en_US:
'Maximum number of tokens to generate (leave 0 for model default)',
zh_Hans: '生成的最大令牌数(设为 0 使用模型默认值)',
},
required: false,
default: 0,
},
{
id: 'output_format',
name: 'output_format',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Output Format',
zh_Hans: '输出格式',
},
description: {
en_US: 'Expected format of the model output',
zh_Hans: '模型输出的预期格式',
},
required: false,
default: 'text',
options: [
{ name: 'text', label: { en_US: 'Plain Text', zh_Hans: '纯文本' } },
{ name: 'json', label: { en_US: 'JSON', zh_Hans: 'JSON' } },
{
name: 'markdown',
label: { en_US: 'Markdown', zh_Hans: 'Markdown 文本' },
},
],
},
{
id: 'json_schema',
name: 'json_schema',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'JSON Schema',
zh_Hans: 'JSON Schema',
},
description: {
en_US: 'JSON schema for structured output validation (optional)',
zh_Hans: '用于结构化输出验证的 JSON Schema可选',
},
required: false,
default: '',
show_if: {
field: 'output_format',
operator: 'eq',
value: 'json',
},
},
{
id: 'enable_tools',
name: 'enable_tools',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Enable Tools',
zh_Hans: '启用工具',
},
description: {
en_US: 'Allow the model to use function calling tools',
zh_Hans: '允许模型使用函数调用工具',
},
required: false,
default: false,
},
{
id: 'tools',
name: 'tools',
type: DynamicFormItemType.TOOLS_SELECTOR,
label: {
en_US: 'Tools',
zh_Hans: '工具',
},
description: {
en_US: 'Select tools that the model can use',
zh_Hans: '选择模型可以使用的工具',
},
required: false,
default: [],
show_if: {
field: 'enable_tools',
operator: 'eq',
value: true,
},
},
],
defaultConfig: {
model: '',
system_prompt: '',
user_prompt_template: '{{input}}',
temperature: 0.7,
max_tokens: 0,
output_format: 'text',
json_schema: '',
enable_tools: false,
tools: [],
},
};
/**
* Question Classifier Node
* Classifies user questions into predefined categories
*/
export const questionClassifierConfig: NodeConfigMeta = {
nodeType: 'question_classifier',
label: {
en_US: 'Question Classifier',
zh_Hans: '问题分类器',
},
description: {
en_US: 'Classify user questions into predefined categories using AI',
zh_Hans: '使用 AI 将用户问题分类到预定义的类别中',
},
icon: 'Tags',
category: 'process',
color: '#8b5cf6',
inputs: [
createInput('question', 'string', {
description: 'The question to classify',
label: { en_US: 'Question', zh_Hans: '问题' },
}),
],
outputs: [
createOutput('category', 'string', {
description: 'The classified category',
label: { en_US: 'Category', zh_Hans: '分类' },
}),
createOutput('confidence', 'number', {
description: 'Classification confidence score (0-1)',
label: { en_US: 'Confidence', zh_Hans: '置信度' },
}),
createOutput('all_scores', 'object', {
description: 'Scores for all categories',
label: { en_US: 'All Scores', zh_Hans: '所有分数' },
}),
],
configSchema: [
{
id: 'model',
name: 'model',
type: DynamicFormItemType.LLM_MODEL_SELECTOR,
label: {
en_US: 'Classification Model',
zh_Hans: '分类模型',
},
description: {
en_US: 'Select the model to use for classification',
zh_Hans: '选择用于分类的模型',
},
required: true,
default: '',
},
{
id: 'categories',
name: 'categories',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Categories Definition',
zh_Hans: '分类定义',
},
description: {
en_US:
'Define categories in JSON format: [{"name": "category1", "description": "...", "examples": ["..."]}]',
zh_Hans:
'使用 JSON 格式定义分类: [{"name": "分类1", "description": "...", "examples": ["..."]}]',
},
required: true,
default: '[]',
},
{
id: 'confidence_threshold',
name: 'confidence_threshold',
type: DynamicFormItemType.FLOAT,
label: {
en_US: 'Confidence Threshold',
zh_Hans: '置信度阈值',
},
description: {
en_US: 'Minimum confidence score required (0.0-1.0)',
zh_Hans: '所需的最小置信度分数0.0-1.0',
},
required: false,
default: 0.7,
},
{
id: 'fallback_category',
name: 'fallback_category',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Fallback Category',
zh_Hans: '默认分类',
},
description: {
en_US: 'Category to use when confidence is below threshold',
zh_Hans: '当置信度低于阈值时使用的分类',
},
required: false,
default: 'other',
},
],
defaultConfig: {
model: '',
categories: '[]',
confidence_threshold: 0.7,
fallback_category: 'other',
},
};
/**
* Parameter Extractor Node
* Extracts structured parameters from natural language
*/
export const parameterExtractorConfig: NodeConfigMeta = {
nodeType: 'parameter_extractor',
label: {
en_US: 'Parameter Extractor',
zh_Hans: '参数提取器',
},
description: {
en_US: 'Extract structured parameters from natural language text using AI',
zh_Hans: '使用 AI 从自然语言文本中提取结构化参数',
},
icon: 'FileSearch',
category: 'process',
color: '#8b5cf6',
inputs: [
createInput('text', 'string', {
description: 'Text to extract parameters from',
label: { en_US: 'Text', zh_Hans: '文本' },
}),
],
outputs: [
createOutput('parameters', 'object', {
description: 'Extracted parameters as key-value pairs',
label: { en_US: 'Parameters', zh_Hans: '参数' },
}),
createOutput('missing', 'array', {
description: 'List of required parameters that could not be extracted',
label: { en_US: 'Missing', zh_Hans: '缺失项' },
}),
createOutput('success', 'boolean', {
description: 'Whether all required parameters were extracted',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'model',
name: 'model',
type: DynamicFormItemType.LLM_MODEL_SELECTOR,
label: {
en_US: 'Extraction Model',
zh_Hans: '提取模型',
},
description: {
en_US: 'Select the model to use for parameter extraction',
zh_Hans: '选择用于参数提取的模型',
},
required: true,
default: '',
},
{
id: 'parameters',
name: 'parameters',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Parameters Schema',
zh_Hans: '参数架构',
},
description: {
en_US:
'JSON array defining expected parameters: [{"name": "date", "type": "string", "description": "Meeting date", "required": true}]',
zh_Hans:
'定义期望参数的 JSON 数组: [{"name": "日期", "type": "string", "description": "会议日期", "required": true}]',
},
required: true,
default: '[]',
},
{
id: 'extraction_prompt',
name: 'extraction_prompt',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Extraction Prompt',
zh_Hans: '提取提示',
},
description: {
en_US: 'Additional instructions for the extraction model',
zh_Hans: '提取模型的额外指令',
},
required: false,
default: '',
},
{
id: 'strict_mode',
name: 'strict_mode',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Strict Mode',
zh_Hans: '严格模式',
},
description: {
en_US: 'Fail if any required parameter cannot be extracted',
zh_Hans: '如果任何必需参数无法提取则失败',
},
required: false,
default: true,
},
],
defaultConfig: {
model: '',
parameters_definition: '[]',
extraction_prompt: '',
strict_mode: true,
},
};
/**
* Knowledge Retrieval Node
* Retrieves relevant information from knowledge bases
*/
export const knowledgeRetrievalConfig: NodeConfigMeta = {
nodeType: 'knowledge_retrieval',
label: {
en_US: 'Knowledge Retrieval',
zh_Hans: '知识检索',
},
description: {
en_US:
'Retrieve relevant information from knowledge bases using semantic search',
zh_Hans: '使用语义搜索从知识库中检索相关信息',
},
icon: 'BookOpen',
category: 'process',
color: '#8b5cf6',
inputs: [
createInput('query', 'string', {
description: 'Query text to search for',
label: { en_US: 'Query', zh_Hans: '查询' },
}),
],
outputs: [
createOutput('results', 'array', {
description: 'Retrieved documents/chunks',
label: { en_US: 'Results', zh_Hans: '结果' },
}),
createOutput('context', 'string', {
description: 'Concatenated text from all results',
label: { en_US: 'Context', zh_Hans: '上下文' },
}),
createOutput('scores', 'array', {
description: 'Similarity scores for each result',
label: { en_US: 'Scores', zh_Hans: '分数' },
}),
],
configSchema: [
{
id: 'knowledge_bases',
name: 'knowledge_bases',
type: DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR,
label: {
en_US: 'Knowledge Bases',
zh_Hans: '知识库',
},
description: {
en_US: 'Select knowledge bases to search',
zh_Hans: '选择要搜索的知识库',
},
required: true,
default: [],
},
{
id: 'top_k',
name: 'top_k',
type: DynamicFormItemType.INT,
label: {
en_US: 'Top K Results',
zh_Hans: '返回数量 (Top K)',
},
description: {
en_US: 'Number of top results to retrieve',
zh_Hans: '返回的最相关结果数量',
},
required: false,
default: 5,
},
{
id: 'similarity_threshold',
name: 'similarity_threshold',
type: DynamicFormItemType.FLOAT,
label: {
en_US: 'Similarity Threshold',
zh_Hans: '相似度阈值',
},
description: {
en_US: 'Minimum similarity score (0.0-1.0) for results to be included',
zh_Hans: '结果被包含的最小相似度分数0.0-1.0',
},
required: false,
default: 0.5,
},
{
id: 'retrieval_mode',
name: 'retrieval_mode',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Retrieval Mode',
zh_Hans: '检索模式',
},
description: {
en_US: 'Method used for retrieving documents',
zh_Hans: '用于检索文档的方法',
},
required: false,
default: 'vector',
options: [
{
name: 'vector',
label: { en_US: 'Vector Search', zh_Hans: '向量检索' },
},
{
name: 'hybrid',
label: { en_US: 'Hybrid Search', zh_Hans: '混合检索' },
},
{
name: 'keyword',
label: { en_US: 'Keyword Search', zh_Hans: '关键词检索' },
},
],
},
{
id: 'rerank_enabled',
name: 'rerank_enabled',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Enable Reranking',
zh_Hans: '启用重排序',
},
description: {
en_US: 'Use a reranking model to improve result relevance',
zh_Hans: '使用重排序模型提高结果相关性',
},
required: false,
default: false,
},
{
id: 'rerank_model',
name: 'rerank_model',
type: DynamicFormItemType.RERANK_MODEL_SELECTOR,
label: {
en_US: 'Rerank Model',
zh_Hans: '重排序模型',
},
description: {
en_US: 'Model to use for reranking results',
zh_Hans: '用于结果重排序的模型',
},
required: false,
default: '',
show_if: {
field: 'rerank_enabled',
operator: 'eq',
value: true,
},
},
],
defaultConfig: {
knowledge_bases: [],
top_k: 5,
similarity_threshold: 0.5,
retrieval_mode: 'vector',
rerank_enabled: false,
rerank_model: '',
},
};
/**
* Text Embedding Node
* Generates vector embeddings for text
*/
export const textEmbeddingConfig: NodeConfigMeta = {
nodeType: 'text_embedding',
label: {
en_US: 'Text Embedding',
zh_Hans: '文本嵌入',
},
description: {
en_US: 'Generate vector embeddings for text using an embedding model',
zh_Hans: '使用嵌入模型为文本生成向量嵌入',
},
icon: 'Binary',
category: 'process',
color: '#8b5cf6',
inputs: [
createInput('text', 'string', {
description: 'Text to embed',
label: { en_US: 'Text', zh_Hans: '文本' },
}),
],
outputs: [
createOutput('embedding', 'array', {
description: 'Vector embedding array',
label: { en_US: 'Embedding', zh_Hans: '嵌入向量' },
}),
createOutput('dimensions', 'number', {
description: 'Number of dimensions in the embedding',
label: { en_US: 'Dimensions', zh_Hans: '维度数' },
}),
],
configSchema: [
{
id: 'model',
name: 'model',
type: DynamicFormItemType.EMBEDDING_MODEL_SELECTOR,
label: {
en_US: 'Embedding Model',
zh_Hans: '嵌入模型',
},
description: {
en_US: 'Select the embedding model to use',
zh_Hans: '选择要使用的嵌入模型',
},
required: true,
default: '',
},
],
defaultConfig: {
model: '',
},
};
/**
* Intent Recognition Node
* Recognizes user intent from natural language
*/
export const intentRecognitionConfig: NodeConfigMeta = {
nodeType: 'intent_recognition',
label: {
en_US: 'Intent Recognition',
zh_Hans: '意图识别',
},
description: {
en_US: 'Recognize user intent from natural language using AI',
zh_Hans: '使用 AI 从自然语言中识别用户意图',
},
icon: 'Target',
category: 'process',
color: '#8b5cf6',
inputs: [
createInput('text', 'string', {
description: 'Text to analyze',
label: { en_US: 'Text', zh_Hans: '文本' },
}),
],
outputs: [
createOutput('intent', 'string', {
description: 'Recognized intent',
label: { en_US: 'Intent', zh_Hans: '意图' },
}),
createOutput('confidence', 'number', {
description: 'Recognition confidence score',
label: { en_US: 'Confidence', zh_Hans: '置信度' },
}),
createOutput('entities', 'object', {
description: 'Extracted entities from the text',
label: { en_US: 'Entities', zh_Hans: '实体' },
}),
],
configSchema: [
{
id: 'model',
name: 'model',
type: DynamicFormItemType.LLM_MODEL_SELECTOR,
label: {
en_US: 'Recognition Model',
zh_Hans: '识别模型',
},
description: {
en_US: 'Select the model for intent recognition',
zh_Hans: '选择用于意图识别的模型',
},
required: true,
default: '',
},
{
id: 'intents_definition',
name: 'intents_definition',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Intents Definition',
zh_Hans: '意图定义',
},
description: {
en_US:
'Define intents in JSON format: [{"name": "intent1", "description": "...", "examples": ["..."]}]',
zh_Hans:
'使用 JSON 格式定义意图: [{"name": "意图1", "description": "...", "examples": ["..."]}]',
},
required: true,
default: '[]',
},
{
id: 'extract_entities',
name: 'extract_entities',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Extract Entities',
zh_Hans: '提取实体',
},
description: {
en_US: 'Also extract named entities from the text',
zh_Hans: '同时从文本中提取命名实体',
},
required: false,
default: true,
},
],
defaultConfig: {
model: '',
intents_definition: '[]',
extract_entities: true,
},
};
/**
* All AI node configurations
*/
export const aiConfigs: NodeConfigMeta[] = [
llmCallConfig,
questionClassifierConfig,
parameterExtractorConfig,
knowledgeRetrievalConfig,
textEmbeddingConfig,
intentRecognitionConfig,
];
/**
* Get AI config by type
*/
export function getAIConfig(nodeType: string): NodeConfigMeta | undefined {
return aiConfigs.find((config) => config.nodeType === nodeType);
}

View File

@@ -1,998 +0,0 @@
/**
* Control Node Configurations
*
* Defines configurations for flow control node types:
* - condition: Conditional branching
* - switch_case: Multi-way branching
* - loop: Loop/iteration
* - parallel: Parallel execution
* - wait: Wait/delay
* - end: End workflow
*/
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
import { NodeConfigMeta, createInput, createOutput } from './types';
/**
* Condition Node
* Conditional branching based on expression
*/
export const conditionConfig: NodeConfigMeta = {
nodeType: 'condition',
label: {
en_US: 'Condition',
zh_Hans: '条件分支',
},
description: {
en_US: 'Branch workflow based on a condition',
zh_Hans: '根据条件分支工作流',
},
icon: 'GitBranch',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('input', 'any', {
description: 'Input data for condition evaluation',
label: { en_US: 'Input', zh_Hans: '输入' },
}),
],
outputs: [
createOutput('true', 'any', {
description: 'Output when condition is true',
label: { en_US: 'True', zh_Hans: '真' },
}),
createOutput('false', 'any', {
description: 'Output when condition is false',
label: { en_US: 'False', zh_Hans: '假' },
}),
],
configSchema: [
{
id: 'condition_type',
name: 'condition_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Condition Type',
zh_Hans: '条件类型',
},
description: {
en_US: 'Type of condition to evaluate',
zh_Hans: '要评估的条件类型',
},
required: true,
default: 'expression',
options: [
{
name: 'expression',
label: { en_US: 'Expression', zh_Hans: '表达式' },
},
{ name: 'comparison', label: { en_US: 'Comparison', zh_Hans: '比较' } },
{ name: 'exists', label: { en_US: 'Value Exists', zh_Hans: '值存在' } },
{
name: 'type_check',
label: { en_US: 'Type Check', zh_Hans: '类型检查' },
},
],
},
{
id: 'expression',
name: 'expression',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Expression',
zh_Hans: '表达式',
},
description: {
en_US: 'JavaScript expression that evaluates to true/false',
zh_Hans: '评估为 true/false 的 JavaScript 表达式',
},
required: true,
default: '',
show_if: {
field: 'condition_type',
operator: 'eq',
value: 'expression',
},
},
{
id: 'left_value',
name: 'left_value',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Left Value',
zh_Hans: '左值',
},
description: {
en_US: 'Left side of comparison (supports variable references)',
zh_Hans: '比较的左侧(支持变量引用)',
},
required: true,
default: '{{input}}',
show_if: {
field: 'condition_type',
operator: 'in',
value: ['comparison', 'exists', 'type_check'],
},
},
{
id: 'operator',
name: 'operator',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Operator',
zh_Hans: '运算符',
},
description: {
en_US: 'Comparison operator',
zh_Hans: '比较运算符',
},
required: true,
default: 'eq',
options: [
{ name: 'eq', label: { en_US: 'Equals (==)', zh_Hans: '等于 (==)' } },
{
name: 'neq',
label: { en_US: 'Not Equals (!=)', zh_Hans: '不等于 (!=)' },
},
{
name: 'gt',
label: { en_US: 'Greater Than (>)', zh_Hans: '大于 (>)' },
},
{
name: 'gte',
label: { en_US: 'Greater or Equal (>=)', zh_Hans: '大于等于 (>=)' },
},
{ name: 'lt', label: { en_US: 'Less Than (<)', zh_Hans: '小于 (<)' } },
{
name: 'lte',
label: { en_US: 'Less or Equal (<=)', zh_Hans: '小于等于 (<=)' },
},
{ name: 'contains', label: { en_US: 'Contains', zh_Hans: '包含' } },
{
name: 'starts_with',
label: { en_US: 'Starts With', zh_Hans: '以...开头' },
},
{
name: 'ends_with',
label: { en_US: 'Ends With', zh_Hans: '以...结尾' },
},
{
name: 'matches',
label: { en_US: 'Matches Regex', zh_Hans: '匹配正则' },
},
],
show_if: {
field: 'condition_type',
operator: 'eq',
value: 'comparison',
},
},
{
id: 'right_value',
name: 'right_value',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Right Value',
zh_Hans: '右值',
},
description: {
en_US: 'Right side of comparison',
zh_Hans: '比较的右侧',
},
required: true,
default: '',
show_if: {
field: 'condition_type',
operator: 'eq',
value: 'comparison',
},
},
{
id: 'expected_type',
name: 'expected_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Expected Type',
zh_Hans: '期望类型',
},
description: {
en_US: 'The type to check for',
zh_Hans: '要检查的类型',
},
required: true,
default: 'string',
options: [
{ name: 'string', label: { en_US: 'String', zh_Hans: '字符串' } },
{ name: 'number', label: { en_US: 'Number', zh_Hans: '数字' } },
{ name: 'boolean', label: { en_US: 'Boolean', zh_Hans: '布尔' } },
{ name: 'object', label: { en_US: 'Object', zh_Hans: '对象' } },
{ name: 'array', label: { en_US: 'Array', zh_Hans: '数组' } },
{ name: 'null', label: { en_US: 'Null', zh_Hans: '空' } },
],
show_if: {
field: 'condition_type',
operator: 'eq',
value: 'type_check',
},
},
],
defaultConfig: {
condition_type: 'expression',
expression: '',
left_value: '{{input}}',
operator: 'eq',
right_value: '',
expected_type: 'string',
},
};
/**
* Switch Case Node
* Multi-way branching based on value
*/
export const switchCaseConfig: NodeConfigMeta = {
nodeType: 'switch_case',
label: {
en_US: 'Switch',
zh_Hans: '多路分支',
},
description: {
en_US: 'Branch workflow based on multiple cases',
zh_Hans: '根据多个条件分支工作流',
},
icon: 'GitFork',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('input', 'any', {
description: 'Value to switch on',
label: { en_US: 'Input', zh_Hans: '输入' },
}),
],
outputs: [
createOutput('case_1', 'any', {
description: 'Branch 1 output',
label: { en_US: 'Branch 1', zh_Hans: '分支 1' },
}),
createOutput('case_2', 'any', {
description: 'Branch 2 output',
label: { en_US: 'Branch 2', zh_Hans: '分支 2' },
}),
createOutput('default', 'any', {
description: 'Default branch output',
label: { en_US: 'Default Branch', zh_Hans: '默认分支' },
}),
],
configSchema: [
{
id: 'switch_expression',
name: 'switch_expression',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Switch Expression',
zh_Hans: '开关表达式',
},
description: {
en_US: 'Expression to evaluate for switching (e.g., {{input.type}})',
zh_Hans: '用于切换的表达式(例如 {{input.type}}',
},
required: true,
default: '{{input}}',
},
{
id: 'cases',
name: 'cases',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Cases',
zh_Hans: '情况',
},
description: {
en_US:
'Define cases as JSON array: [{"name": "case_1", "value": "value1"}, {"name": "case_2", "values": ["v1", "v2"]}]',
zh_Hans:
'使用 JSON 数组定义情况: [{"name": "case_1", "value": "value1"}, {"name": "case_2", "values": ["v1", "v2"]}]',
},
required: true,
default:
'[{"name": "case_1", "value": ""}, {"name": "case_2", "value": ""}]',
},
{
id: 'case_sensitive',
name: 'case_sensitive',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Case Sensitive',
zh_Hans: '区分大小写',
},
description: {
en_US: 'Whether string comparisons are case-sensitive',
zh_Hans: '字符串比较是否区分大小写',
},
required: false,
default: true,
},
],
defaultConfig: {
switch_expression: '{{input}}',
cases: '[{"name": "case_1", "value": ""}, {"name": "case_2", "value": ""}]',
case_sensitive: true,
},
};
/**
* Loop Node
* Iterates over items or until condition
*/
export const loopConfig: NodeConfigMeta = {
nodeType: 'loop',
label: {
en_US: 'Loop',
zh_Hans: '循环',
},
description: {
en_US: 'Iterate over items or repeat until condition',
zh_Hans: '遍历项目或重复直到满足条件',
},
icon: 'Repeat',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('items', 'array', {
description: 'Items to iterate over (for each loop)',
label: { en_US: 'Items', zh_Hans: '项目' },
required: false,
}),
],
outputs: [
createOutput('item', 'any', {
description: 'Current item in iteration',
label: { en_US: 'Item', zh_Hans: '当前项' },
}),
createOutput('index', 'number', {
description: 'Current iteration index',
label: { en_US: 'Index', zh_Hans: '索引' },
}),
createOutput('completed', 'any', {
description: 'Output after loop completes',
label: { en_US: 'Completed', zh_Hans: '完成' },
}),
],
configSchema: [
{
id: 'loop_type',
name: 'loop_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Loop Type',
zh_Hans: '循环类型',
},
description: {
en_US: 'Type of loop to execute',
zh_Hans: '要执行的循环类型',
},
required: true,
default: 'foreach',
options: [
{ name: 'foreach', label: { en_US: 'For Each', zh_Hans: '逐项遍历' } },
{ name: 'while', label: { en_US: 'While', zh_Hans: '条件循环' } },
{ name: 'count', label: { en_US: 'Count', zh_Hans: '计数' } },
],
},
{
id: 'max_iterations',
name: 'max_iterations',
type: DynamicFormItemType.INT,
label: {
en_US: 'Max Iterations',
zh_Hans: '最大迭代次数',
},
description: {
en_US: 'Maximum number of iterations (safety limit)',
zh_Hans: '最大迭代次数(安全限制)',
},
required: false,
default: 100,
},
{
id: 'count',
name: 'count',
type: DynamicFormItemType.INT,
label: {
en_US: 'Count',
zh_Hans: '计数',
},
description: {
en_US: 'Number of times to iterate',
zh_Hans: '迭代次数',
},
required: true,
default: 10,
show_if: {
field: 'loop_type',
operator: 'eq',
value: 'count',
},
},
{
id: 'while_condition',
name: 'while_condition',
type: DynamicFormItemType.STRING,
label: {
en_US: 'While Condition',
zh_Hans: 'While 条件',
},
description: {
en_US: 'Condition expression to continue looping',
zh_Hans: '继续循环的条件表达式',
},
required: true,
default: '',
show_if: {
field: 'loop_type',
operator: 'eq',
value: 'while',
},
},
{
id: 'parallel',
name: 'parallel',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Parallel Execution',
zh_Hans: '并行执行',
},
description: {
en_US: 'Execute iterations in parallel',
zh_Hans: '并行执行迭代',
},
required: false,
default: false,
show_if: {
field: 'loop_type',
operator: 'eq',
value: 'foreach',
},
},
{
id: 'parallel_limit',
name: 'parallel_limit',
type: DynamicFormItemType.INT,
label: {
en_US: 'Parallel Limit',
zh_Hans: '并行限制',
},
description: {
en_US: 'Maximum number of parallel executions',
zh_Hans: '最大并行执行数',
},
required: false,
default: 5,
show_if: {
field: 'parallel',
operator: 'eq',
value: true,
},
},
],
defaultConfig: {
loop_type: 'foreach',
max_iterations: 100,
count: 10,
while_condition: '',
parallel: false,
parallel_limit: 5,
},
};
/**
* Parallel Node
* Execute multiple branches in parallel
*/
export const parallelConfig: NodeConfigMeta = {
nodeType: 'parallel',
label: {
en_US: 'Parallel',
zh_Hans: '并行执行',
},
description: {
en_US: 'Execute multiple branches in parallel',
zh_Hans: '并行执行多个分支',
},
icon: 'GitMerge',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('input', 'any', {
description: 'Input data for all branches',
label: { en_US: 'Input', zh_Hans: '输入' },
}),
],
outputs: [
createOutput('branch_1', 'any', {
description: 'Branch 1 output',
label: { en_US: 'Branch 1', zh_Hans: '分支 1' },
}),
createOutput('branch_2', 'any', {
description: 'Branch 2 output',
label: { en_US: 'Branch 2', zh_Hans: '分支 2' },
}),
createOutput('results', 'object', {
description: 'Combined results from all branches',
label: { en_US: 'Results', zh_Hans: '结果' },
}),
],
configSchema: [
{
id: 'branches',
name: 'branches',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Branches',
zh_Hans: '分支',
},
description: {
en_US:
'Define branches as JSON array: [{"name": "branch_1"}, {"name": "branch_2"}]',
zh_Hans:
'使用 JSON 数组定义分支: [{"name": "branch_1"}, {"name": "branch_2"}]',
},
required: true,
default: '[{"name": "branch_1"}, {"name": "branch_2"}]',
},
{
id: 'wait_for_all',
name: 'wait_for_all',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Wait for All',
zh_Hans: '等待全部完成',
},
description: {
en_US: 'Wait for all branches to complete before continuing',
zh_Hans: '等待所有分支完成后再继续',
},
required: false,
default: true,
},
{
id: 'fail_fast',
name: 'fail_fast',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Fail Fast',
zh_Hans: '快速失败',
},
description: {
en_US: 'Stop all branches if any one fails',
zh_Hans: '如果任何一个分支失败则停止所有分支',
},
required: false,
default: false,
},
],
defaultConfig: {
branches: '[{"name": "branch_1"}, {"name": "branch_2"}]',
wait_for_all: true,
fail_fast: false,
},
};
/**
* Wait Node
* Pause workflow execution
*/
export const waitConfig: NodeConfigMeta = {
nodeType: 'wait',
label: {
en_US: 'Wait',
zh_Hans: '等待',
},
description: {
en_US: 'Pause workflow execution for a specified duration or condition',
zh_Hans: '暂停工作流执行指定的时间或等待条件满足',
},
icon: 'Clock',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('input', 'any', {
description: 'Input to pass through',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
],
outputs: [
createOutput('output', 'any', {
description: 'Passed through input',
label: { en_US: 'Output', zh_Hans: '输出' },
}),
],
configSchema: [
{
id: 'wait_type',
name: 'wait_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Wait Type',
zh_Hans: '等待类型',
},
description: {
en_US: 'Type of wait operation',
zh_Hans: '等待操作的类型',
},
required: true,
default: 'duration',
options: [
{ name: 'duration', label: { en_US: 'Duration', zh_Hans: '时长' } },
{ name: 'until', label: { en_US: 'Until Time', zh_Hans: '直到时间' } },
],
},
{
id: 'duration',
name: 'duration',
type: DynamicFormItemType.INT,
label: {
en_US: 'Duration (seconds)',
zh_Hans: '时长(秒)',
},
description: {
en_US: 'Number of seconds to wait',
zh_Hans: '等待的秒数',
},
required: true,
default: 5,
show_if: {
field: 'wait_type',
operator: 'eq',
value: 'duration',
},
},
{
id: 'until_time',
name: 'until_time',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Until Time',
zh_Hans: '直到时间',
},
description: {
en_US: 'Wait until this time (ISO 8601 format or expression)',
zh_Hans: '等待直到此时间ISO 8601 格式或表达式)',
},
required: true,
default: '',
show_if: {
field: 'wait_type',
operator: 'eq',
value: 'until',
},
},
],
defaultConfig: {
wait_type: 'duration',
duration: 5,
until_time: '',
},
};
/**
* End Node
* Terminates workflow execution
*/
export const endConfig: NodeConfigMeta = {
nodeType: 'end',
label: {
en_US: 'End',
zh_Hans: '结束',
},
description: {
en_US: 'End the workflow execution',
zh_Hans: '结束工作流执行',
},
icon: 'CircleStop',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('input', 'any', {
description: 'Final output data',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
],
outputs: [],
configSchema: [
{
id: 'status',
name: 'status',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'End Status',
zh_Hans: '结束状态',
},
description: {
en_US: 'Status to report when workflow ends',
zh_Hans: '工作流结束时报告的状态',
},
required: true,
default: 'success',
options: [
{ name: 'success', label: { en_US: 'Success', zh_Hans: '成功' } },
{ name: 'failed', label: { en_US: 'Failed', zh_Hans: '失败' } },
{ name: 'cancelled', label: { en_US: 'Cancelled', zh_Hans: '取消' } },
],
},
{
id: 'message',
name: 'message',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Message',
zh_Hans: '消息',
},
description: {
en_US: 'Optional message to include with the end status',
zh_Hans: '与结束状态一起包含的可选消息',
},
required: false,
default: '',
},
],
defaultConfig: {
status: 'success',
message: '',
},
};
/**
* Iterator Node
* Iterates over array items one by one
*/
export const iteratorConfig: NodeConfigMeta = {
nodeType: 'iterator',
label: {
en_US: 'Iterator',
zh_Hans: '迭代器',
},
description: {
en_US: 'Iterate over array elements one by one',
zh_Hans: '逐个遍历数组元素',
},
icon: 'Repeat',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('items', 'array', {
description: 'Array to iterate over',
label: { en_US: 'Items', zh_Hans: '项目' },
}),
],
outputs: [
createOutput('item', 'any', {
description: 'Current item',
label: { en_US: 'Item', zh_Hans: '当前项' },
}),
createOutput('index', 'number', {
description: 'Current index',
label: { en_US: 'Index', zh_Hans: '索引' },
}),
createOutput('is_first', 'boolean', {
description: 'Whether this is the first item',
label: { en_US: 'Is First', zh_Hans: '是否第一个' },
}),
createOutput('is_last', 'boolean', {
description: 'Whether this is the last item',
label: { en_US: 'Is Last', zh_Hans: '是否最后一个' },
}),
createOutput('completed', 'any', {
description: 'Output after iteration completes',
label: { en_US: 'Completed', zh_Hans: '完成' },
}),
],
configSchema: [
{
id: 'parallel',
name: 'parallel',
type: DynamicFormItemType.BOOLEAN,
label: { en_US: 'Parallel Processing', zh_Hans: '并行处理' },
description: {
en_US: 'Process items in parallel',
zh_Hans: '并行处理项目',
},
required: false,
default: false,
},
{
id: 'max_concurrency',
name: 'max_concurrency',
type: DynamicFormItemType.INT,
label: { en_US: 'Max Concurrency', zh_Hans: '最大并发数' },
description: {
en_US: 'Maximum number of concurrent iterations',
zh_Hans: '最大并发迭代数',
},
required: false,
default: 5,
show_if: { field: 'parallel', operator: 'eq', value: true },
},
{
id: 'max_iterations',
name: 'max_iterations',
type: DynamicFormItemType.INT,
label: { en_US: 'Max Iterations', zh_Hans: '最大迭代次数' },
description: {
en_US: 'Safety limit on iterations',
zh_Hans: '迭代次数安全限制',
},
required: false,
default: 1000,
},
],
defaultConfig: { parallel: false, max_concurrency: 5, max_iterations: 1000 },
};
/**
* Merge Node
* Merges multiple branches back together
*/
export const mergeConfig: NodeConfigMeta = {
nodeType: 'merge',
label: {
en_US: 'Merge',
zh_Hans: '合并',
},
description: {
en_US: 'Merge multiple branches back together',
zh_Hans: '将多个分支合并在一起',
},
icon: 'GitMerge',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('branch_1', 'any', {
description: 'Input from branch 1',
label: { en_US: 'Branch 1', zh_Hans: '分支 1' },
required: false,
}),
createInput('branch_2', 'any', {
description: 'Input from branch 2',
label: { en_US: 'Branch 2', zh_Hans: '分支 2' },
required: false,
}),
],
outputs: [
createOutput('output', 'any', {
description: 'Merged output',
label: { en_US: 'Output', zh_Hans: '输出' },
}),
],
configSchema: [
{
id: 'merge_strategy',
name: 'merge_strategy',
type: DynamicFormItemType.SELECT,
label: { en_US: 'Merge Strategy', zh_Hans: '合并策略' },
description: {
en_US: 'How to merge inputs from branches',
zh_Hans: '如何合并分支输入',
},
required: true,
default: 'wait_all',
options: [
{
name: 'wait_all',
label: { en_US: 'Wait for All', zh_Hans: '等待全部' },
},
{
name: 'first_completed',
label: { en_US: 'First Completed', zh_Hans: '第一个完成' },
},
{
name: 'combine',
label: { en_US: 'Combine to Object', zh_Hans: '合并为对象' },
},
{
name: 'array',
label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' },
},
],
},
],
defaultConfig: { merge_strategy: 'wait_all' },
};
/**
* Variable Aggregator Node
* Aggregates variable outputs from multiple branches
*/
export const variableAggregatorConfig: NodeConfigMeta = {
nodeType: 'variable_aggregator',
label: {
en_US: 'Variable Aggregator',
zh_Hans: '变量聚合器',
},
description: {
en_US: 'Aggregate variable outputs from multiple branches',
zh_Hans: '聚合多个分支的变量输出',
},
icon: 'GitMerge',
category: 'control',
color: '#8b5cf6',
inputs: [
createInput('input', 'any', {
description: 'Input data',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
],
outputs: [
createOutput('output', 'any', {
description: 'Aggregated output',
label: { en_US: 'Output', zh_Hans: '输出' },
}),
],
configSchema: [
{
id: 'variable_mappings',
name: 'variable_mappings',
type: DynamicFormItemType.TEXT,
label: { en_US: 'Variable Mappings', zh_Hans: '变量映射' },
description: {
en_US:
'JSON mapping of output variables: {"out_key": "{{nodes.xxx.value}}"}',
zh_Hans: 'JSON 格式的输出变量映射: {"out_key": "{{nodes.xxx.value}}"}',
},
required: true,
default: '{}',
},
{
id: 'aggregation_mode',
name: 'aggregation_mode',
type: DynamicFormItemType.SELECT,
label: { en_US: 'Aggregation Mode', zh_Hans: '聚合模式' },
description: {
en_US: 'How to aggregate the variables',
zh_Hans: '如何聚合变量',
},
required: true,
default: 'merge',
options: [
{
name: 'merge',
label: { en_US: 'Merge Objects', zh_Hans: '合并对象' },
},
{
name: 'array',
label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' },
},
{
name: 'first',
label: { en_US: 'First Non-null', zh_Hans: '第一个非空' },
},
],
},
],
defaultConfig: { variable_mappings: '{}', aggregation_mode: 'merge' },
};
/**
* All control node configurations
*/
export const controlConfigs: NodeConfigMeta[] = [
conditionConfig,
switchCaseConfig,
loopConfig,
iteratorConfig,
parallelConfig,
waitConfig,
mergeConfig,
variableAggregatorConfig,
endConfig,
];
/**
* Get control config by type
*/
export function getControlConfig(nodeType: string): NodeConfigMeta | undefined {
return controlConfigs.find((config) => config.nodeType === nodeType);
}

View File

@@ -1,261 +0,0 @@
/**
* Node Configurations Index
*
* This module exports all node configuration metadata and provides
* utility functions for accessing node configurations.
*/
// Types
export * from './types';
// Trigger Nodes
export {
triggerConfigs,
getTriggerConfig,
messageTriggerConfig,
cronTriggerConfig,
webhookTriggerConfig,
eventTriggerConfig,
} from './trigger-configs';
// AI Nodes
export {
aiConfigs,
getAIConfig,
llmCallConfig,
questionClassifierConfig,
parameterExtractorConfig,
knowledgeRetrievalConfig,
textEmbeddingConfig,
intentRecognitionConfig,
} from './ai-configs';
// Process Nodes
export {
processConfigs,
getProcessConfig,
textTemplateConfig,
jsonTransformConfig,
codeExecutorConfig,
dataAggregatorConfig,
textSplitterConfig,
variableAssignmentConfig,
dataTransformConfig,
} from './process-configs';
// Control Nodes
export {
controlConfigs,
getControlConfig,
conditionConfig,
switchCaseConfig,
loopConfig,
iteratorConfig,
parallelConfig,
waitConfig,
mergeConfig,
variableAggregatorConfig,
endConfig,
} from './control-configs';
// Action Nodes
export {
actionConfigs,
getActionConfig,
sendMessageConfig,
replyMessageConfig,
httpRequestConfig,
storeDataConfig,
callPipelineConfig,
setVariableConfig,
openingStatementConfig,
botInvokeConfig,
workflowInvokeConfig,
notificationConfig,
} from './action-configs';
// Integration Nodes
export {
integrationConfigs,
getIntegrationConfig,
difyWorkflowConfig,
difyKnowledgeQueryConfig,
n8nWorkflowConfig,
langflowFlowConfig,
cozeBotConfig,
databaseQueryConfig,
redisOperationConfig,
mcpToolConfig,
memoryStoreConfig,
} from './integration-configs';
import { NodeConfigMeta, NodeConfigRegistry } from './types';
import { triggerConfigs } from './trigger-configs';
import { aiConfigs } from './ai-configs';
import { processConfigs } from './process-configs';
import { controlConfigs } from './control-configs';
import { actionConfigs } from './action-configs';
import { integrationConfigs } from './integration-configs';
import { NodeCategory } from '@/app/infra/entities/workflow';
/**
* All node configurations combined
*/
export const allNodeConfigs: NodeConfigMeta[] = [
...triggerConfigs,
...aiConfigs,
...processConfigs,
...controlConfigs,
...actionConfigs,
...integrationConfigs,
];
/**
* Node configuration registry by type
* Registers each config under both its short name (e.g. "message_trigger")
* and its full category-prefixed name (e.g. "trigger.message_trigger")
* so lookups from PropertyPanel / useWorkflowStore always succeed.
*/
export const nodeConfigRegistry: NodeConfigRegistry = (() => {
const registry: NodeConfigRegistry = {};
for (const config of allNodeConfigs) {
// Short name
registry[config.nodeType] = config;
// Full category.name
registry[`${config.category}.${config.nodeType}`] = config;
}
// Aliases for nodes whose palette type differs from config nodeType
// control.switch -> switch_case config
if (registry['switch_case']) {
registry['switch'] = registry['switch_case'];
registry['control.switch'] = registry['switch_case'];
}
// action.end also points to the end config in control
if (registry['end']) {
registry['action.end'] = registry['end'];
}
return registry;
})();
/**
* Get node configuration by type
*/
export function getNodeConfig(nodeType: string): NodeConfigMeta | undefined {
return nodeConfigRegistry[nodeType];
}
/**
* Get all node configurations for a category
*/
export function getNodeConfigsByCategory(
category: NodeCategory,
): NodeConfigMeta[] {
return allNodeConfigs.filter((config) => config.category === category);
}
/**
* Get all entry point node configurations (trigger nodes)
*/
export function getEntryPointConfigs(): NodeConfigMeta[] {
return allNodeConfigs.filter((config) => config.isEntryPoint);
}
/**
* Check if a node type exists
*/
export function isValidNodeType(nodeType: string): boolean {
return nodeType in nodeConfigRegistry;
}
/**
* Get default configuration for a node type
*/
export function getDefaultConfig(nodeType: string): Record<string, unknown> {
const config = getNodeConfig(nodeType);
if (!config) return {};
// Build default config from schema defaults
const defaults: Record<string, unknown> = {};
for (const field of config.configSchema) {
defaults[field.name] = field.default;
}
// Override with explicit defaultConfig if provided
if (config.defaultConfig) {
Object.assign(defaults, config.defaultConfig);
}
return defaults;
}
/**
* Validate node configuration against schema
*/
export function validateNodeConfig(
nodeType: string,
config: Record<string, unknown>,
): { valid: boolean; errors: string[] } {
const nodeConfig = getNodeConfig(nodeType);
if (!nodeConfig) {
return { valid: false, errors: [`Unknown node type: ${nodeType}`] };
}
const errors: string[] = [];
for (const field of nodeConfig.configSchema) {
const value = config[field.name];
// Check required fields
if (
field.required &&
(value === undefined || value === null || value === '')
) {
errors.push(`Field "${field.name}" is required`);
continue;
}
// Skip validation for optional empty fields
if (!field.required && (value === undefined || value === null)) {
continue;
}
// Type-specific validation could be added here
}
return { valid: errors.length === 0, errors };
}
/**
* Convert node config metadata to NodeTypeMetadata format
* (for compatibility with existing workflow store)
*/
export function toNodeTypeMetadata(config: NodeConfigMeta) {
return {
type: config.nodeType,
name: config.label,
description: config.description,
category: config.category,
icon: config.icon,
color: config.color,
inputs: config.inputs.map((input) => ({
name: input.name,
type: input.type,
description: input.description,
required: input.required,
})),
outputs: config.outputs.map((output) => ({
name: output.name,
type: output.type,
description: output.description,
required: output.required,
})),
config_schema: config.configSchema,
};
}
/**
* Convert all node configs to NodeTypeMetadata format
*/
export function getAllNodeTypeMetadata() {
return allNodeConfigs.map(toNodeTypeMetadata);
}

View File

@@ -1,912 +0,0 @@
/**
* Integration Node Configurations
*
* Defines configurations for integration node types:
* - database_query: Query databases
* - redis_operation: Redis operations
* - mcp_tool: MCP tool invocation
*/
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
import { NodeConfigMeta, createInput, createOutput } from './types';
/**
* Database Query Node
* Executes database queries
*/
export const databaseQueryConfig: NodeConfigMeta = {
nodeType: 'database_query',
label: {
en_US: 'Database Query',
zh_Hans: '数据库查询',
},
description: {
en_US: 'Execute database queries',
zh_Hans: '执行数据库查询',
},
icon: 'Database',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('parameters', 'object', {
description: 'Query parameters',
label: { en_US: 'Parameters', zh_Hans: '参数' },
required: false,
}),
],
outputs: [
createOutput('results', 'array', {
description: 'Query results',
label: { en_US: 'Results', zh_Hans: '结果' },
}),
createOutput('row_count', 'number', {
description: 'Number of rows affected/returned',
label: { en_US: 'Row Count', zh_Hans: '行数' },
}),
createOutput('success', 'boolean', {
description: 'Whether query was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'connection_type',
name: 'connection_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Database Type',
zh_Hans: '数据库类型',
},
description: {
en_US: 'Type of database to connect to',
zh_Hans: '要连接的数据库类型',
},
required: true,
default: 'postgresql',
options: [
{
name: 'postgresql',
label: { en_US: 'PostgreSQL', zh_Hans: 'PostgreSQL' },
},
{ name: 'mysql', label: { en_US: 'MySQL', zh_Hans: 'MySQL' } },
{ name: 'sqlite', label: { en_US: 'SQLite', zh_Hans: 'SQLite' } },
],
},
{
id: 'connection_string',
name: 'connection_string',
type: DynamicFormItemType.SECRET,
label: {
en_US: 'Connection String',
zh_Hans: '连接字符串',
},
description: {
en_US: 'Database connection string',
zh_Hans: '数据库连接字符串',
},
required: true,
default: '',
},
{
id: 'query',
name: 'query',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'SQL Query',
zh_Hans: 'SQL 查询',
},
description: {
en_US: 'SQL query to execute (use $1, $2, etc. for parameters)',
zh_Hans: '要执行的 SQL 查询(使用 $1、$2 等作为参数占位符)',
},
required: true,
default: '',
},
{
id: 'query_type',
name: 'query_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Query Type',
zh_Hans: '查询类型',
},
description: {
en_US: 'Type of query operation',
zh_Hans: '查询操作的类型',
},
required: true,
default: 'select',
options: [
{ name: 'select', label: { en_US: 'SELECT', zh_Hans: 'SELECT' } },
{ name: 'insert', label: { en_US: 'INSERT', zh_Hans: 'INSERT' } },
{ name: 'update', label: { en_US: 'UPDATE', zh_Hans: 'UPDATE' } },
{ name: 'delete', label: { en_US: 'DELETE', zh_Hans: 'DELETE' } },
],
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: {
en_US: 'Timeout (seconds)',
zh_Hans: '超时时间(秒)',
},
description: {
en_US: 'Query timeout',
zh_Hans: '查询超时时间',
},
required: false,
default: 30,
},
],
defaultConfig: {
connection_type: 'postgresql',
connection_string: '',
query: '',
query_type: 'select',
timeout: 30,
},
};
/**
* Redis Operation Node
* Performs Redis operations
*/
export const redisOperationConfig: NodeConfigMeta = {
nodeType: 'redis_operation',
label: {
en_US: 'Redis Operation',
zh_Hans: 'Redis 操作',
},
description: {
en_US: 'Perform Redis cache operations',
zh_Hans: '执行 Redis 缓存操作',
},
icon: 'Server',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('key', 'string', {
description: 'Redis key',
label: { en_US: 'Key', zh_Hans: '键' },
required: false,
}),
createInput('value', 'any', {
description: 'Value to store',
label: { en_US: 'Value', zh_Hans: '值' },
required: false,
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Operation result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether operation was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'connection_url',
name: 'connection_url',
type: DynamicFormItemType.SECRET,
label: {
en_US: 'Redis URL',
zh_Hans: 'Redis URL',
},
description: {
en_US: 'Redis connection URL (e.g., redis://localhost:6379)',
zh_Hans: 'Redis 连接 URL例如 redis://localhost:6379',
},
required: true,
default: 'redis://localhost:6379',
},
{
id: 'operation',
name: 'operation',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Operation',
zh_Hans: '操作',
},
description: {
en_US: 'Redis operation to perform',
zh_Hans: '要执行的 Redis 操作',
},
required: true,
default: 'get',
options: [
{ name: 'get', label: { en_US: 'GET', zh_Hans: 'GET' } },
{ name: 'set', label: { en_US: 'SET', zh_Hans: 'SET' } },
{ name: 'delete', label: { en_US: 'DELETE', zh_Hans: 'DELETE' } },
{ name: 'exists', label: { en_US: 'EXISTS', zh_Hans: 'EXISTS' } },
{ name: 'incr', label: { en_US: 'INCR', zh_Hans: 'INCR' } },
{ name: 'decr', label: { en_US: 'DECR', zh_Hans: 'DECR' } },
{ name: 'hget', label: { en_US: 'HGET', zh_Hans: 'HGET' } },
{ name: 'hset', label: { en_US: 'HSET', zh_Hans: 'HSET' } },
{ name: 'lpush', label: { en_US: 'LPUSH', zh_Hans: 'LPUSH' } },
{ name: 'rpush', label: { en_US: 'RPUSH', zh_Hans: 'RPUSH' } },
{ name: 'lpop', label: { en_US: 'LPOP', zh_Hans: 'LPOP' } },
{ name: 'rpop', label: { en_US: 'RPOP', zh_Hans: 'RPOP' } },
],
},
{
id: 'key_template',
name: 'key_template',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Key Template',
zh_Hans: '键模板',
},
description: {
en_US: 'Redis key (supports variable interpolation)',
zh_Hans: 'Redis 键(支持变量插值)',
},
required: false,
default: '',
},
{
id: 'hash_field',
name: 'hash_field',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Hash Field',
zh_Hans: '哈希字段',
},
description: {
en_US: 'Field name for hash operations',
zh_Hans: '哈希操作的字段名',
},
required: false,
default: '',
show_if: {
field: 'operation',
operator: 'in',
value: ['hget', 'hset'],
},
},
{
id: 'ttl',
name: 'ttl',
type: DynamicFormItemType.INT,
label: {
en_US: 'TTL (seconds)',
zh_Hans: 'TTL',
},
description: {
en_US: 'Time to live for SET operations (0 = no expiry)',
zh_Hans: 'SET 操作的过期时间0 = 不过期)',
},
required: false,
default: 0,
show_if: {
field: 'operation',
operator: 'eq',
value: 'set',
},
},
],
defaultConfig: {
connection_url: 'redis://localhost:6379',
operation: 'get',
key_template: '',
hash_field: '',
ttl: 0,
},
};
/**
* MCP Tool Node
* Invokes MCP (Model Context Protocol) tools
*/
export const mcpToolConfig: NodeConfigMeta = {
nodeType: 'mcp_tool',
label: {
en_US: 'MCP Tool',
zh_Hans: 'MCP 工具',
},
description: {
en_US: 'Invoke an MCP (Model Context Protocol) tool',
zh_Hans: '调用 MCP模型上下文协议工具',
},
icon: 'Wrench',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('arguments', 'object', {
description: 'Tool arguments',
label: { en_US: 'Arguments', zh_Hans: '参数' },
required: false,
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Tool execution result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether tool call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
createOutput('error', 'string', {
description: 'Error message if failed',
label: { en_US: 'Error', zh_Hans: '错误' },
}),
],
configSchema: [
{
id: 'server_name',
name: 'server_name',
type: DynamicFormItemType.STRING,
label: {
en_US: 'MCP Server',
zh_Hans: 'MCP 服务器',
},
description: {
en_US: 'Name of the MCP server',
zh_Hans: 'MCP 服务器名称',
},
required: true,
default: '',
},
{
id: 'tool_name',
name: 'tool_name',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Tool Name',
zh_Hans: '工具名称',
},
description: {
en_US: 'Name of the MCP tool to invoke',
zh_Hans: '要调用的 MCP 工具名称',
},
required: true,
default: '',
},
{
id: 'arguments_template',
name: 'arguments_template',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Arguments Template',
zh_Hans: '参数模板',
},
description: {
en_US:
'Tool arguments as JSON (supports variable interpolation). Leave empty to use input.',
zh_Hans: '工具参数JSON 格式,支持变量插值)。留空则使用输入。',
},
required: false,
default: '',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: {
en_US: 'Timeout (seconds)',
zh_Hans: '超时时间(秒)',
},
description: {
en_US: 'Maximum execution time',
zh_Hans: '最大执行时间',
},
required: false,
default: 30,
},
],
defaultConfig: {
server_name: '',
tool_name: '',
arguments_template: '',
timeout: 30,
},
};
/**
* Memory Store Node
* Store and retrieve from workflow memory
*/
export const memoryStoreConfig: NodeConfigMeta = {
nodeType: 'memory_store',
label: {
en_US: 'Memory Store',
zh_Hans: '记忆存储',
},
description: {
en_US: 'Store and retrieve data from workflow memory',
zh_Hans: '从工作流记忆中存储和检索数据',
},
icon: 'HardDrive',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('value', 'any', {
description: 'Value to store',
label: { en_US: 'Value', zh_Hans: '值' },
required: false,
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Retrieved or stored value',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether operation was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'operation',
name: 'operation',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Operation',
zh_Hans: '操作',
},
description: {
en_US: 'Memory operation to perform',
zh_Hans: '要执行的记忆操作',
},
required: true,
default: 'get',
options: [
{ name: 'get', label: { en_US: 'Get', zh_Hans: '获取' } },
{ name: 'set', label: { en_US: 'Set', zh_Hans: '设置' } },
{ name: 'delete', label: { en_US: 'Delete', zh_Hans: '删除' } },
{ name: 'append', label: { en_US: 'Append', zh_Hans: '追加' } },
{ name: 'list', label: { en_US: 'List All', zh_Hans: '列出全部' } },
],
},
{
id: 'key',
name: 'key',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Key',
zh_Hans: '键',
},
description: {
en_US: 'Memory key (supports variable interpolation)',
zh_Hans: '记忆键(支持变量插值)',
},
required: true,
default: '',
show_if: {
field: 'operation',
operator: 'neq',
value: 'list',
},
},
{
id: 'scope',
name: 'scope',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Scope',
zh_Hans: '作用域',
},
description: {
en_US: 'Scope of the memory storage',
zh_Hans: '记忆存储的作用域',
},
required: true,
default: 'execution',
options: [
{ name: 'execution', label: { en_US: 'Execution', zh_Hans: '执行' } },
{ name: 'workflow', label: { en_US: 'Workflow', zh_Hans: '工作流' } },
{ name: 'session', label: { en_US: 'Session', zh_Hans: '会话' } },
{ name: 'user', label: { en_US: 'User', zh_Hans: '用户' } },
{ name: 'global', label: { en_US: 'Global', zh_Hans: '全局' } },
],
},
{
id: 'ttl',
name: 'ttl',
type: DynamicFormItemType.INT,
label: {
en_US: 'TTL (seconds)',
zh_Hans: 'TTL',
},
description: {
en_US: 'Time to live (0 = no expiry)',
zh_Hans: '过期时间0 = 不过期)',
},
required: false,
default: 0,
show_if: {
field: 'operation',
operator: 'eq',
value: 'set',
},
},
],
defaultConfig: {
operation: 'get',
key: '',
scope: 'execution',
ttl: 0,
},
};
/**
* Dify Workflow Node
* Calls Dify platform workflow
*/
export const difyWorkflowConfig: NodeConfigMeta = {
nodeType: 'dify_workflow',
label: { en_US: 'Dify Workflow', zh_Hans: 'Dify 工作流' },
description: {
en_US: 'Call a Dify platform workflow',
zh_Hans: '调用 Dify 平台工作流',
},
icon: 'Bot',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('input', 'any', {
description: 'Input data',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Workflow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'base-url',
name: 'base-url',
type: DynamicFormItemType.STRING,
label: { en_US: 'Base URL', zh_Hans: 'Base URL' },
description: { en_US: 'Dify API base URL', zh_Hans: 'Dify API 基础 URL' },
required: true,
default: '',
},
{
id: 'api-key',
name: 'api-key',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Key', zh_Hans: 'API Key' },
description: { en_US: 'Dify API key', zh_Hans: 'Dify API 密钥' },
required: true,
default: '',
},
{
id: 'app-type',
name: 'app-type',
type: DynamicFormItemType.SELECT,
label: { en_US: 'App Type', zh_Hans: '应用类型' },
description: { en_US: 'Dify application type', zh_Hans: 'Dify 应用类型' },
required: true,
default: 'workflow',
options: [
{ name: 'workflow', label: { en_US: 'Workflow', zh_Hans: '工作流' } },
{ name: 'chatbot', label: { en_US: 'Chatbot', zh_Hans: '聊天机器人' } },
],
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' },
description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' },
required: false,
default: 60,
},
],
defaultConfig: {
'base-url': '',
'api-key': '',
'app-type': 'workflow',
timeout: 60,
},
};
/**
* Dify Knowledge Query Node
*/
export const difyKnowledgeQueryConfig: NodeConfigMeta = {
nodeType: 'dify_knowledge_query',
label: { en_US: 'Dify Knowledge Query', zh_Hans: 'Dify 知识库查询' },
description: {
en_US: 'Query Dify knowledge base',
zh_Hans: '查询 Dify 知识库',
},
icon: 'Search',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('query', 'string', {
description: 'Search query',
label: { en_US: 'Query', zh_Hans: '查询' },
}),
],
outputs: [
createOutput('results', 'array', {
description: 'Search results',
label: { en_US: 'Results', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether query was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'base-url',
name: 'base-url',
type: DynamicFormItemType.STRING,
label: { en_US: 'Base URL', zh_Hans: 'Base URL' },
description: { en_US: 'Dify API base URL', zh_Hans: 'Dify API 基础 URL' },
required: true,
default: '',
},
{
id: 'api-key',
name: 'api-key',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Key', zh_Hans: 'API Key' },
description: { en_US: 'Dify API key', zh_Hans: 'Dify API 密钥' },
required: true,
default: '',
},
{
id: 'dataset_id',
name: 'dataset_id',
type: DynamicFormItemType.STRING,
label: { en_US: 'Dataset ID', zh_Hans: '数据集 ID' },
description: { en_US: 'Dify dataset ID', zh_Hans: 'Dify 数据集 ID' },
required: true,
default: '',
},
{
id: 'top_k',
name: 'top_k',
type: DynamicFormItemType.INT,
label: { en_US: 'Top K', zh_Hans: 'Top K' },
description: {
en_US: 'Number of results to return',
zh_Hans: '返回结果数量',
},
required: false,
default: 5,
},
],
defaultConfig: { 'base-url': '', 'api-key': '', dataset_id: '', top_k: 5 },
};
/**
* N8n Workflow Node
*/
export const n8nWorkflowConfig: NodeConfigMeta = {
nodeType: 'n8n_workflow',
label: { en_US: 'N8n Workflow', zh_Hans: 'n8n 工作流' },
description: {
en_US: 'Call an n8n workflow via webhook',
zh_Hans: '通过 webhook 调用 n8n 工作流',
},
icon: 'Settings',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('input', 'any', {
description: 'Input data',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Workflow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'webhook-url',
name: 'webhook-url',
type: DynamicFormItemType.STRING,
label: { en_US: 'Webhook URL', zh_Hans: 'Webhook URL' },
description: { en_US: 'N8n webhook URL', zh_Hans: 'n8n Webhook URL' },
required: true,
default: '',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' },
description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' },
required: false,
default: 60,
},
],
defaultConfig: { 'webhook-url': '', timeout: 60 },
};
/**
* Langflow Flow Node
*/
export const langflowFlowConfig: NodeConfigMeta = {
nodeType: 'langflow_flow',
label: { en_US: 'Langflow Flow', zh_Hans: 'Langflow 流程' },
description: { en_US: 'Call a Langflow flow', zh_Hans: '调用 Langflow 流程' },
icon: 'Workflow',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('input', 'any', {
description: 'Input data',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Flow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'base-url',
name: 'base-url',
type: DynamicFormItemType.STRING,
label: { en_US: 'Base URL', zh_Hans: 'Base URL' },
description: {
en_US: 'Langflow API base URL',
zh_Hans: 'Langflow API 基础 URL',
},
required: true,
default: '',
},
{
id: 'flow-id',
name: 'flow-id',
type: DynamicFormItemType.STRING,
label: { en_US: 'Flow ID', zh_Hans: '流程 ID' },
description: { en_US: 'Langflow flow ID', zh_Hans: 'Langflow 流程 ID' },
required: true,
default: '',
},
{
id: 'api-key',
name: 'api-key',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Key', zh_Hans: 'API Key' },
description: {
en_US: 'Langflow API key (optional)',
zh_Hans: 'Langflow API 密钥(可选)',
},
required: false,
default: '',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' },
description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' },
required: false,
default: 60,
},
],
defaultConfig: { 'base-url': '', 'flow-id': '', 'api-key': '', timeout: 60 },
};
/**
* Coze Bot Node
*/
export const cozeBotConfig: NodeConfigMeta = {
nodeType: 'coze_bot',
label: { en_US: 'Coze Bot', zh_Hans: 'Coze Bot' },
description: { en_US: 'Call a Coze Bot', zh_Hans: '调用扣子 Bot' },
icon: 'Bot',
category: 'integration',
color: '#ec4899',
inputs: [
createInput('message', 'string', {
description: 'Message to send',
label: { en_US: 'Message', zh_Hans: '消息' },
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Bot response',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
configSchema: [
{
id: 'api-base',
name: 'api-base',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Base URL', zh_Hans: 'API 基础 URL' },
description: { en_US: 'Coze API base URL', zh_Hans: 'Coze API 基础 URL' },
required: true,
default: 'https://api.coze.com',
},
{
id: 'bot-id',
name: 'bot-id',
type: DynamicFormItemType.STRING,
label: { en_US: 'Bot ID', zh_Hans: 'Bot ID' },
description: { en_US: 'Coze Bot ID', zh_Hans: 'Coze Bot ID' },
required: true,
default: '',
},
{
id: 'api-key',
name: 'api-key',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Key', zh_Hans: 'API Key' },
description: { en_US: 'Coze API key', zh_Hans: 'Coze API 密钥' },
required: true,
default: '',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' },
description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' },
required: false,
default: 60,
},
],
defaultConfig: {
'api-base': 'https://api.coze.com',
'bot-id': '',
'api-key': '',
timeout: 60,
},
};
/**
* All integration node configurations
*/
export const integrationConfigs: NodeConfigMeta[] = [
difyWorkflowConfig,
difyKnowledgeQueryConfig,
n8nWorkflowConfig,
langflowFlowConfig,
cozeBotConfig,
databaseQueryConfig,
redisOperationConfig,
mcpToolConfig,
memoryStoreConfig,
];
/**
* Get integration config by type
*/
export function getIntegrationConfig(
nodeType: string,
): NodeConfigMeta | undefined {
return integrationConfigs.find((config) => config.nodeType === nodeType);
}

View File

@@ -1,833 +0,0 @@
/**
* Process Node Configurations
*
* Defines configurations for general processing node types:
* - text_template: Generate text using templates
* - json_transform: Transform JSON data
* - code_executor: Execute custom code
* - data_aggregator: Aggregate data from multiple sources
* - text_splitter: Split text into chunks
*/
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
import { NodeConfigMeta, createInput, createOutput } from './types';
/**
* Text Template Node
* Generates text using variable interpolation
*/
export const textTemplateConfig: NodeConfigMeta = {
nodeType: 'text_template',
label: {
en_US: 'Text Template',
zh_Hans: '文本模板',
},
description: {
en_US: 'Generate text using templates with variable interpolation',
zh_Hans: '使用带有变量插值的模板生成文本',
},
icon: 'FileText',
category: 'process',
color: '#3b82f6',
inputs: [
createInput('variables', 'object', {
description: 'Variables to use in the template',
label: { en_US: 'Variables', zh_Hans: '变量' },
required: false,
}),
],
outputs: [
createOutput('text', 'string', {
description: 'Generated text',
label: { en_US: 'Text', zh_Hans: '文本' },
}),
],
configSchema: [
{
id: 'template',
name: 'template',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Template',
zh_Hans: '模板',
},
description: {
en_US:
'Text template with variable placeholders (e.g., {{variable_name}})',
zh_Hans: '带有变量占位符的文本模板(例如 {{variable_name}}',
},
required: true,
default: '',
},
{
id: 'escape_html',
name: 'escape_html',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Escape HTML',
zh_Hans: '转义 HTML',
},
description: {
en_US: 'Escape HTML characters in variable values',
zh_Hans: '转义变量值中的 HTML 字符',
},
required: false,
default: false,
},
{
id: 'trim_whitespace',
name: 'trim_whitespace',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Trim Whitespace',
zh_Hans: '去除空白',
},
description: {
en_US: 'Remove leading and trailing whitespace from output',
zh_Hans: '去除输出的前后空白',
},
required: false,
default: true,
},
],
defaultConfig: {
template: '',
escape_html: false,
trim_whitespace: true,
},
};
/**
* JSON Transform Node
* Transforms JSON data using JSONPath or JMESPath expressions
*/
export const jsonTransformConfig: NodeConfigMeta = {
nodeType: 'json_transform',
label: {
en_US: 'JSON Transform',
zh_Hans: 'JSON 转换',
},
description: {
en_US: 'Transform JSON data using expressions or mappings',
zh_Hans: '使用表达式或映射转换 JSON 数据',
},
icon: 'Braces',
category: 'process',
color: '#3b82f6',
inputs: [
createInput('input', 'object', {
description: 'JSON data to transform',
label: { en_US: 'Input', zh_Hans: '输入' },
}),
],
outputs: [
createOutput('output', 'any', {
description: 'Transformed data',
label: { en_US: 'Output', zh_Hans: '输出' },
}),
],
configSchema: [
{
id: 'transform_type',
name: 'transform_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Transform Type',
zh_Hans: '转换类型',
},
description: {
en_US: 'Method of transformation',
zh_Hans: '转换方法',
},
required: true,
default: 'jmespath',
options: [
{
name: 'jmespath',
label: { en_US: 'JMESPath Expression', zh_Hans: 'JMESPath 表达式' },
},
{
name: 'jsonpath',
label: { en_US: 'JSONPath Expression', zh_Hans: 'JSONPath 表达式' },
},
{
name: 'mapping',
label: { en_US: 'Field Mapping', zh_Hans: '字段映射' },
},
],
},
{
id: 'expression',
name: 'expression',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Expression',
zh_Hans: '表达式',
},
description: {
en_US: 'JMESPath or JSONPath expression',
zh_Hans: 'JMESPath 或 JSONPath 表达式',
},
required: true,
default: '',
show_if: {
field: 'transform_type',
operator: 'in',
value: ['jmespath', 'jsonpath'],
},
},
{
id: 'mapping',
name: 'mapping',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Field Mapping',
zh_Hans: '字段映射',
},
description: {
en_US:
'JSON object defining field mappings: {"output_field": "input.path.to.field"}',
zh_Hans:
'定义字段映射的 JSON 对象: {"output_field": "input.path.to.field"}',
},
required: true,
default: '{}',
show_if: {
field: 'transform_type',
operator: 'eq',
value: 'mapping',
},
},
],
defaultConfig: {
transform_type: 'jmespath',
expression: '',
mapping: '{}',
},
};
/**
* Code Executor Node
* Executes custom code (JavaScript/Python)
*/
export const codeExecutorConfig: NodeConfigMeta = {
nodeType: 'code_executor',
label: {
en_US: 'Code Executor',
zh_Hans: '代码执行',
},
description: {
en_US: 'Execute custom code to process data',
zh_Hans: '执行自定义代码处理数据',
},
icon: 'Code',
category: 'process',
color: '#3b82f6',
inputs: [
createInput('input', 'any', {
description: 'Input data for the code',
label: { en_US: 'Input', zh_Hans: '输入' },
}),
],
outputs: [
createOutput('output', 'any', {
description: 'Code execution result',
label: { en_US: 'Output', zh_Hans: '输出' },
}),
createOutput('logs', 'array', {
description: 'Console logs from code execution',
label: { en_US: 'Logs', zh_Hans: '日志' },
}),
],
configSchema: [
{
id: 'language',
name: 'language',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Language',
zh_Hans: '语言',
},
description: {
en_US: 'Programming language to use',
zh_Hans: '要使用的编程语言',
},
required: true,
default: 'javascript',
options: [
{
name: 'javascript',
label: { en_US: 'JavaScript', zh_Hans: 'JavaScript' },
},
{ name: 'python', label: { en_US: 'Python', zh_Hans: 'Python' } },
],
},
{
id: 'code',
name: 'code',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Code',
zh_Hans: '代码',
},
description: {
en_US:
'Code to execute. Use `input` to access input data and return the result.',
zh_Hans: '要执行的代码。使用 `input` 访问输入数据,并返回结果。',
},
required: true,
default:
'// Access input with: input\n// Return result with: return result;\n\nreturn input;',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: {
en_US: 'Timeout (ms)',
zh_Hans: '超时时间 (毫秒)',
},
description: {
en_US: 'Maximum execution time in milliseconds',
zh_Hans: '最大执行时间(毫秒)',
},
required: false,
default: 5000,
},
],
defaultConfig: {
language: 'javascript',
code: '// Access input with: input\n// Return result with: return result;\n\nreturn input;',
timeout: 5000,
},
};
/**
* Data Aggregator Node
* Aggregates data from multiple inputs
*/
export const dataAggregatorConfig: NodeConfigMeta = {
nodeType: 'data_aggregator',
label: {
en_US: 'Data Aggregator',
zh_Hans: '数据聚合',
},
description: {
en_US: 'Aggregate and combine data from multiple sources',
zh_Hans: '聚合和组合来自多个来源的数据',
},
icon: 'Layers',
category: 'process',
color: '#3b82f6',
inputs: [
createInput('items', 'array', {
description: 'Array of items to aggregate',
label: { en_US: 'Items', zh_Hans: '项目' },
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Aggregated result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('count', 'number', {
description: 'Number of items aggregated',
label: { en_US: 'Count', zh_Hans: '数量' },
}),
],
configSchema: [
{
id: 'aggregation_type',
name: 'aggregation_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Aggregation Type',
zh_Hans: '聚合类型',
},
description: {
en_US: 'How to aggregate the data',
zh_Hans: '如何聚合数据',
},
required: true,
default: 'array',
options: [
{
name: 'array',
label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' },
},
{
name: 'concat',
label: { en_US: 'Concatenate Strings', zh_Hans: '连接字符串' },
},
{ name: 'sum', label: { en_US: 'Sum Numbers', zh_Hans: '求和' } },
{
name: 'average',
label: { en_US: 'Average Numbers', zh_Hans: '求平均' },
},
{ name: 'min', label: { en_US: 'Minimum', zh_Hans: '最小值' } },
{ name: 'max', label: { en_US: 'Maximum', zh_Hans: '最大值' } },
{
name: 'merge',
label: { en_US: 'Merge Objects', zh_Hans: '合并对象' },
},
{ name: 'first', label: { en_US: 'First Item', zh_Hans: '第一项' } },
{ name: 'last', label: { en_US: 'Last Item', zh_Hans: '最后一项' } },
],
},
{
id: 'separator',
name: 'separator',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Separator',
zh_Hans: '分隔符',
},
description: {
en_US: 'Separator for string concatenation',
zh_Hans: '字符串连接的分隔符',
},
required: false,
default: '\n',
show_if: {
field: 'aggregation_type',
operator: 'eq',
value: 'concat',
},
},
{
id: 'field_path',
name: 'field_path',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Field Path',
zh_Hans: '字段路径',
},
description: {
en_US: 'Path to the field to aggregate (for objects)',
zh_Hans: '要聚合的字段路径(用于对象)',
},
required: false,
default: '',
},
],
defaultConfig: {
aggregation_type: 'array',
separator: '\n',
field_path: '',
},
};
/**
* Text Splitter Node
* Splits text into chunks
*/
export const textSplitterConfig: NodeConfigMeta = {
nodeType: 'text_splitter',
label: {
en_US: 'Text Splitter',
zh_Hans: '文本分割',
},
description: {
en_US: 'Split text into smaller chunks',
zh_Hans: '将文本分割成较小的块',
},
icon: 'Scissors',
category: 'process',
color: '#3b82f6',
inputs: [
createInput('text', 'string', {
description: 'Text to split',
label: { en_US: 'Text', zh_Hans: '文本' },
}),
],
outputs: [
createOutput('chunks', 'array', {
description: 'Array of text chunks',
label: { en_US: 'Chunks', zh_Hans: '块' },
}),
createOutput('count', 'number', {
description: 'Number of chunks',
label: { en_US: 'Count', zh_Hans: '数量' },
}),
],
configSchema: [
{
id: 'split_type',
name: 'split_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Split Type',
zh_Hans: '分割类型',
},
description: {
en_US: 'How to split the text',
zh_Hans: '如何分割文本',
},
required: true,
default: 'separator',
options: [
{
name: 'separator',
label: { en_US: 'By Separator', zh_Hans: '按分隔符' },
},
{ name: 'length', label: { en_US: 'By Length', zh_Hans: '按长度' } },
{
name: 'sentences',
label: { en_US: 'By Sentences', zh_Hans: '按句子' },
},
{
name: 'paragraphs',
label: { en_US: 'By Paragraphs', zh_Hans: '按段落' },
},
{
name: 'regex',
label: { en_US: 'By Regex', zh_Hans: '按正则表达式' },
},
],
},
{
id: 'separator',
name: 'separator',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Separator',
zh_Hans: '分隔符',
},
description: {
en_US: 'String to split on',
zh_Hans: '用于分割的字符串',
},
required: false,
default: '\n',
show_if: {
field: 'split_type',
operator: 'eq',
value: 'separator',
},
},
{
id: 'chunk_size',
name: 'chunk_size',
type: DynamicFormItemType.INT,
label: {
en_US: 'Chunk Size',
zh_Hans: '块大小',
},
description: {
en_US: 'Maximum characters per chunk',
zh_Hans: '每块的最大字符数',
},
required: false,
default: 1000,
show_if: {
field: 'split_type',
operator: 'eq',
value: 'length',
},
},
{
id: 'chunk_overlap',
name: 'chunk_overlap',
type: DynamicFormItemType.INT,
label: {
en_US: 'Chunk Overlap',
zh_Hans: '块重叠',
},
description: {
en_US: 'Number of characters to overlap between chunks',
zh_Hans: '块之间重叠的字符数',
},
required: false,
default: 100,
show_if: {
field: 'split_type',
operator: 'eq',
value: 'length',
},
},
{
id: 'regex_pattern',
name: 'regex_pattern',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Regex Pattern',
zh_Hans: '正则表达式模式',
},
description: {
en_US: 'Regular expression pattern to split on',
zh_Hans: '用于分割的正则表达式模式',
},
required: false,
default: '',
show_if: {
field: 'split_type',
operator: 'eq',
value: 'regex',
},
},
{
id: 'remove_empty',
name: 'remove_empty',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Remove Empty',
zh_Hans: '移除空块',
},
description: {
en_US: 'Remove empty chunks from result',
zh_Hans: '从结果中移除空块',
},
required: false,
default: true,
},
],
defaultConfig: {
split_type: 'separator',
separator: '\n',
chunk_size: 1000,
chunk_overlap: 100,
regex_pattern: '',
remove_empty: true,
},
};
/**
* Variable Assignment Node
* Assigns values to workflow variables
*/
export const variableAssignmentConfig: NodeConfigMeta = {
nodeType: 'variable_assignment',
label: {
en_US: 'Variable Assignment',
zh_Hans: '变量赋值',
},
description: {
en_US: 'Assign values to workflow variables',
zh_Hans: '为工作流变量赋值',
},
icon: 'Variable',
category: 'process',
color: '#3b82f6',
inputs: [
createInput('value', 'any', {
description: 'Value to assign',
label: { en_US: 'Value', zh_Hans: '值' },
required: false,
}),
],
outputs: [
createOutput('output', 'any', {
description: 'The assigned value',
label: { en_US: 'Output', zh_Hans: '输出' },
}),
],
configSchema: [
{
id: 'variable_name',
name: 'variable_name',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Variable Name',
zh_Hans: '变量名',
},
description: {
en_US: 'Name of the variable to assign',
zh_Hans: '要赋值的变量名',
},
required: true,
default: '',
},
{
id: 'value_type',
name: 'value_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Value Type',
zh_Hans: '值类型',
},
description: {
en_US: 'Type of value to assign',
zh_Hans: '要赋的值类型',
},
required: true,
default: 'input',
options: [
{ name: 'input', label: { en_US: 'From Input', zh_Hans: '来自输入' } },
{ name: 'static', label: { en_US: 'Static Value', zh_Hans: '静态值' } },
{
name: 'expression',
label: { en_US: 'Expression', zh_Hans: '表达式' },
},
],
},
{
id: 'static_value',
name: 'static_value',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Static Value',
zh_Hans: '静态值',
},
description: {
en_US: 'Value to assign (as JSON)',
zh_Hans: '要赋的值JSON 格式)',
},
required: false,
default: '',
show_if: {
field: 'value_type',
operator: 'eq',
value: 'static',
},
},
{
id: 'expression',
name: 'expression',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Expression',
zh_Hans: '表达式',
},
description: {
en_US: 'Expression to evaluate (e.g., {{input}} + 1)',
zh_Hans: '要计算的表达式(例如 {{input}} + 1',
},
required: false,
default: '',
show_if: {
field: 'value_type',
operator: 'eq',
value: 'expression',
},
},
],
defaultConfig: {
variable_name: '',
value_type: 'input',
static_value: '',
expression: '',
},
};
/**
* Data Transform Node
* Transform and extract data using templates or JSONPath
*/
export const dataTransformConfig: NodeConfigMeta = {
nodeType: 'data_transform',
label: {
en_US: 'Data Transform',
zh_Hans: '数据转换',
},
description: {
en_US: 'Transform and extract data using templates or JSONPath',
zh_Hans: '使用模板或 JSONPath 转换和提取数据',
},
icon: 'RefreshCw',
category: 'process',
color: '#3b82f6',
inputs: [
createInput('data', 'any', {
description: 'Input data',
label: { en_US: 'Data', zh_Hans: '数据' },
required: true,
}),
],
outputs: [
createOutput('result', 'any', {
description: 'Transform result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
],
configSchema: [
{
id: 'transform_type',
name: 'transform_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Transform Type',
zh_Hans: '转换类型',
},
description: {
en_US: 'Type of transformation to perform',
zh_Hans: '要执行的转换类型',
},
required: true,
default: 'template',
options: [
{ name: 'template', label: { en_US: 'Template', zh_Hans: '模板' } },
{ name: 'jsonpath', label: { en_US: 'JSONPath', zh_Hans: 'JSONPath' } },
{ name: 'jmespath', label: { en_US: 'JMESPath', zh_Hans: 'JMESPath' } },
{
name: 'expression',
label: { en_US: 'Expression', zh_Hans: '表达式' },
},
],
},
{
id: 'template',
name: 'template',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Template',
zh_Hans: '模板',
},
description: {
en_US: 'Template with {{variable}} syntax',
zh_Hans: '支持 {{variable}} 语法的模板',
},
required: false,
default: '',
show_if: {
field: 'transform_type',
operator: 'eq',
value: 'template',
},
},
{
id: 'expression',
name: 'expression',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Expression',
zh_Hans: '表达式',
},
description: {
en_US: 'JSONPath/JMESPath expression',
zh_Hans: 'JSONPath/JMESPath 表达式',
},
required: false,
default: '',
show_if: {
field: 'transform_type',
operator: 'in',
value: ['jsonpath', 'jmespath', 'expression'],
},
},
],
defaultConfig: {
transform_type: 'template',
template: '',
expression: '',
},
};
/**
* All process node configurations
*/
export const processConfigs: NodeConfigMeta[] = [
textTemplateConfig,
jsonTransformConfig,
codeExecutorConfig,
dataAggregatorConfig,
textSplitterConfig,
variableAssignmentConfig,
dataTransformConfig,
];
/**
* Get process config by type
*/
export function getProcessConfig(nodeType: string): NodeConfigMeta | undefined {
return processConfigs.find((config) => config.nodeType === nodeType);
}

View File

@@ -1,542 +0,0 @@
/**
* Trigger Node Configurations
*
* Defines configurations for all trigger node types:
* - message_trigger: Triggered by incoming messages
* - cron_trigger: Triggered by scheduled time
* - webhook_trigger: Triggered by HTTP webhook calls
* - event_trigger: Triggered by system events
*/
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
import { NodeConfigMeta, createOutput } from './types';
/**
* Message Trigger Node
* Triggers workflow when a message matches specified conditions
*/
export const messageTriggerConfig: NodeConfigMeta = {
nodeType: 'message_trigger',
label: {
en_US: 'Message Trigger',
zh_Hans: '消息触发',
},
description: {
en_US: 'Trigger workflow when a message matches the specified conditions',
zh_Hans: '当收到匹配指定条件的消息时触发工作流',
},
icon: 'MessageSquare',
category: 'trigger',
color: '#f59e0b',
isEntryPoint: true,
maxInstances: 1,
inputs: [],
outputs: [
createOutput('message', 'object', {
description: 'The received message object',
label: { en_US: 'Message', zh_Hans: '消息' },
}),
createOutput('sender', 'object', {
description: 'Message sender information',
label: { en_US: 'Sender', zh_Hans: '发送者' },
}),
createOutput('context', 'object', {
description: 'Message context information',
label: { en_US: 'Context', zh_Hans: '上下文' },
}),
],
configSchema: [
{
id: 'match_type',
name: 'match_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Match Type',
zh_Hans: '匹配类型',
},
description: {
en_US: 'How to match the incoming message',
zh_Hans: '如何匹配收到的消息',
},
required: true,
default: 'all',
options: [
{ name: 'all', label: { en_US: 'All Messages', zh_Hans: '所有消息' } },
{
name: 'prefix',
label: { en_US: 'Prefix Match', zh_Hans: '前缀匹配' },
},
{ name: 'regex', label: { en_US: 'Regex Match', zh_Hans: '正则匹配' } },
{
name: 'contains',
label: { en_US: 'Contains Keyword', zh_Hans: '包含关键词' },
},
{ name: 'exact', label: { en_US: 'Exact Match', zh_Hans: '精确匹配' } },
],
},
{
id: 'match_pattern',
name: 'match_pattern',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Match Pattern',
zh_Hans: '匹配模式',
},
description: {
en_US:
'The pattern to match against the message (prefix, regex, keyword, or exact text)',
zh_Hans: '用于匹配消息的模式(前缀、正则表达式、关键词或精确文本)',
},
required: false,
default: '',
show_if: {
field: 'match_type',
operator: 'neq',
value: 'all',
},
},
{
id: 'ignore_bot_messages',
name: 'ignore_bot_messages',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Ignore Bot Messages',
zh_Hans: '忽略机器人消息',
},
description: {
en_US: 'Do not trigger for messages sent by bots',
zh_Hans: '不对机器人发送的消息触发',
},
required: false,
default: true,
},
],
defaultConfig: {
match_type: 'all',
match_pattern: '',
ignore_bot_messages: true,
},
};
/**
* Cron Trigger Node
* Triggers workflow on a schedule
*/
export const cronTriggerConfig: NodeConfigMeta = {
nodeType: 'cron_trigger',
label: {
en_US: 'Scheduled Trigger',
zh_Hans: '定时触发',
},
description: {
en_US: 'Trigger workflow on a scheduled time using cron expression',
zh_Hans: '使用 Cron 表达式按计划时间触发工作流',
},
icon: 'Clock',
category: 'trigger',
color: '#f59e0b',
isEntryPoint: true,
inputs: [],
outputs: [
createOutput('trigger_time', 'datetime', {
description: 'The time when the trigger fired',
label: { en_US: 'Trigger Time', zh_Hans: '触发时间' },
}),
createOutput('context', 'object', {
description: 'Trigger context information',
label: { en_US: 'Context', zh_Hans: '上下文' },
}),
],
configSchema: [
{
id: 'cron_expression',
name: 'cron_expression',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Cron Expression',
zh_Hans: 'Cron 表达式',
},
description: {
en_US: 'Standard cron expression (e.g., "0 9 * * *" for 9 AM daily)',
zh_Hans: '标准 Cron 表达式(例如 "0 9 * * *" 表示每天上午 9 点)',
},
required: true,
default: '0 9 * * *',
},
{
id: 'timezone',
name: 'timezone',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Timezone',
zh_Hans: '时区',
},
description: {
en_US: 'Timezone for the cron schedule',
zh_Hans: 'Cron 计划的时区',
},
required: true,
default: 'Asia/Shanghai',
options: [
{ name: 'UTC', label: { en_US: 'UTC', zh_Hans: 'UTC' } },
{
name: 'Asia/Shanghai',
label: {
en_US: 'Asia/Shanghai (UTC+8)',
zh_Hans: '亚洲/上海 (UTC+8)',
},
},
{
name: 'Asia/Tokyo',
label: { en_US: 'Asia/Tokyo (UTC+9)', zh_Hans: '亚洲/东京 (UTC+9)' },
},
{
name: 'America/New_York',
label: {
en_US: 'America/New_York (EST)',
zh_Hans: '美国/纽约 (EST)',
},
},
{
name: 'America/Los_Angeles',
label: {
en_US: 'America/Los_Angeles (PST)',
zh_Hans: '美国/洛杉矶 (PST)',
},
},
{
name: 'Europe/London',
label: { en_US: 'Europe/London (GMT)', zh_Hans: '欧洲/伦敦 (GMT)' },
},
{
name: 'Europe/Berlin',
label: { en_US: 'Europe/Berlin (CET)', zh_Hans: '欧洲/柏林 (CET)' },
},
],
},
{
id: 'description',
name: 'description',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Description',
zh_Hans: '描述',
},
description: {
en_US: 'Optional description for this scheduled trigger',
zh_Hans: '此定时触发器的可选描述',
},
required: false,
default: '',
},
{
id: 'enabled',
name: 'enabled',
type: DynamicFormItemType.BOOLEAN,
label: {
en_US: 'Enabled',
zh_Hans: '启用',
},
description: {
en_US: 'Whether this scheduled trigger is active',
zh_Hans: '此定时触发器是否激活',
},
required: false,
default: true,
},
],
defaultConfig: {
cron_expression: '0 9 * * *',
timezone: 'Asia/Shanghai',
description: '',
enabled: true,
},
};
/**
* Webhook Trigger Node
* Triggers workflow via HTTP webhook
*/
export const webhookTriggerConfig: NodeConfigMeta = {
nodeType: 'webhook_trigger',
label: {
en_US: 'Webhook Trigger',
zh_Hans: 'Webhook 触发',
},
description: {
en_US:
'Trigger workflow when an HTTP request is received at the webhook URL',
zh_Hans: '当在 Webhook URL 收到 HTTP 请求时触发工作流',
},
icon: 'Webhook',
category: 'trigger',
color: '#f59e0b',
isEntryPoint: true,
inputs: [],
outputs: [
createOutput('body', 'object', {
description: 'Request body data',
label: { en_US: 'Body', zh_Hans: '请求体' },
}),
createOutput('headers', 'object', {
description: 'Request headers',
label: { en_US: 'Headers', zh_Hans: '请求头' },
}),
createOutput('query', 'object', {
description: 'Query parameters',
label: { en_US: 'Query', zh_Hans: '查询参数' },
}),
createOutput('method', 'string', {
description: 'HTTP method',
label: { en_US: 'Method', zh_Hans: 'HTTP 方法' },
}),
],
configSchema: [
{
id: 'webhook_path',
name: 'webhook_path',
type: DynamicFormItemType.STRING,
label: {
en_US: 'Webhook Path',
zh_Hans: 'Webhook 路径',
},
description: {
en_US: 'Unique path for this webhook (e.g., "my-workflow")',
zh_Hans: '此 Webhook 的唯一路径(例如 "my-workflow"',
},
required: true,
default: '',
},
{
id: 'auth_type',
name: 'auth_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Authentication',
zh_Hans: '认证方式',
},
description: {
en_US: 'How to authenticate incoming webhook requests',
zh_Hans: '如何验证传入的 Webhook 请求',
},
required: true,
default: 'none',
options: [
{ name: 'none', label: { en_US: 'None', zh_Hans: '无' } },
{
name: 'token',
label: { en_US: 'Bearer Token', zh_Hans: 'Bearer 令牌' },
},
{
name: 'signature',
label: { en_US: 'Signature', zh_Hans: '签名验证' },
},
{ name: 'basic', label: { en_US: 'Basic Auth', zh_Hans: '基本认证' } },
],
},
{
id: 'auth_token',
name: 'auth_token',
type: DynamicFormItemType.SECRET,
label: {
en_US: 'Auth Token',
zh_Hans: '认证令牌',
},
description: {
en_US: 'Token or secret for authentication',
zh_Hans: '用于认证的令牌或密钥',
},
required: true,
default: '',
show_if: {
field: 'auth_type',
operator: 'in',
value: ['token', 'signature', 'basic'],
},
},
{
id: 'content_type',
name: 'content_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Content Type',
zh_Hans: '内容类型',
},
description: {
en_US: 'Expected Content-Type of the request',
zh_Hans: '请求预期的 Content-Type',
},
required: false,
default: 'application/json',
options: [
{
name: 'application/json',
label: { en_US: 'application/json', zh_Hans: 'JSON' },
},
{
name: 'application/x-www-form-urlencoded',
label: {
en_US: 'application/x-www-form-urlencoded',
zh_Hans: '表单编码',
},
},
{
name: 'multipart/form-data',
label: { en_US: 'multipart/form-data', zh_Hans: '表单数据' },
},
{
name: 'text/plain',
label: { en_US: 'text/plain', zh_Hans: '纯文本' },
},
],
},
{
id: 'validation',
name: 'validation',
type: DynamicFormItemType.TEXT,
label: {
en_US: 'Validation Rules',
zh_Hans: '验证规则',
},
description: {
en_US: 'JSON validation rules for request body (optional)',
zh_Hans: '请求体的 JSON 验证规则(可选)',
},
required: false,
default: '{}',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: {
en_US: 'Timeout (seconds)',
zh_Hans: '超时时间(秒)',
},
description: {
en_US: 'Request timeout in seconds',
zh_Hans: '请求超时时间(秒)',
},
required: false,
default: 30,
},
],
defaultConfig: {
webhook_path: '',
auth_type: 'none',
auth_token: '',
content_type: 'application/json',
validation: '{}',
timeout: 30,
},
};
/**
* Event Trigger Node
* Triggers workflow on system events
*/
export const eventTriggerConfig: NodeConfigMeta = {
nodeType: 'event_trigger',
label: {
en_US: 'Event Trigger',
zh_Hans: '事件触发',
},
description: {
en_US: 'Trigger workflow when a system event occurs',
zh_Hans: '当系统事件发生时触发工作流',
},
icon: 'Zap',
category: 'trigger',
color: '#f59e0b',
isEntryPoint: true,
inputs: [],
outputs: [
createOutput('event', 'object', {
description: 'The event data',
label: { en_US: 'Event', zh_Hans: '事件' },
}),
createOutput('event_type', 'string', {
description: 'Type of the event',
label: { en_US: 'Event Type', zh_Hans: '事件类型' },
}),
createOutput('context', 'object', {
description: 'Event context information',
label: { en_US: 'Context', zh_Hans: '上下文' },
}),
],
configSchema: [
{
id: 'event_type',
name: 'event_type',
type: DynamicFormItemType.SELECT,
label: {
en_US: 'Event Type',
zh_Hans: '事件类型',
},
description: {
en_US: 'The type of system event to listen for',
zh_Hans: '要监听的系统事件类型',
},
required: true,
default: 'member_join',
options: [
{
name: 'member_join',
label: { en_US: 'Member Join', zh_Hans: '成员加入' },
},
{
name: 'member_leave',
label: { en_US: 'Member Leave', zh_Hans: '成员离开' },
},
{
name: 'message_recall',
label: { en_US: 'Message Recall', zh_Hans: '消息撤回' },
},
{
name: 'group_created',
label: { en_US: 'Group Created', zh_Hans: '群组创建' },
},
{
name: 'group_disbanded',
label: { en_US: 'Group Disbanded', zh_Hans: '群组解散' },
},
{
name: 'bot_added',
label: { en_US: 'Bot Added to Group', zh_Hans: '机器人被添加到群' },
},
{
name: 'bot_removed',
label: { en_US: 'Bot Removed from Group', zh_Hans: '机器人被移出群' },
},
{
name: 'friend_request',
label: { en_US: 'Friend Request', zh_Hans: '好友请求' },
},
{
name: 'group_request',
label: { en_US: 'Group Join Request', zh_Hans: '入群请求' },
},
],
},
],
defaultConfig: {
event_type: 'member_join',
},
};
/**
* All trigger node configurations
*/
export const triggerConfigs: NodeConfigMeta[] = [
messageTriggerConfig,
cronTriggerConfig,
webhookTriggerConfig,
eventTriggerConfig,
];
/**
* Get trigger config by type
*/
export function getTriggerConfig(nodeType: string): NodeConfigMeta | undefined {
return triggerConfigs.find((config) => config.nodeType === nodeType);
}

View File

@@ -1,120 +0,0 @@
/**
* Workflow Node Configuration Types
*
* This module defines the types used for node configuration metadata.
* It extends the existing dynamic form system to support workflow-specific features.
*/
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
import { I18nObject } from '@/app/infra/entities/common';
import { NodeCategory, PortDefinition } from '@/app/infra/entities/workflow';
/**
* Extended port configuration with additional metadata
*/
export interface ExtendedPortDefinition extends PortDefinition {
label?: I18nObject;
}
/**
* Node configuration metadata
* Defines all aspects of a node type including its appearance, ports, and configuration options
*/
export interface NodeConfigMeta {
/** Unique node type identifier */
nodeType: string;
/** Display name for the node */
label: I18nObject;
/** Description of what the node does */
description: I18nObject;
/** Icon name (from lucide-react) */
icon: string;
/** Node category for organization */
category: NodeCategory;
/** Color for the node header */
color?: string;
/** Input port definitions */
inputs: ExtendedPortDefinition[];
/** Output port definitions */
outputs: ExtendedPortDefinition[];
/** Configuration schema using the dynamic form system */
configSchema: IDynamicFormItemSchema[];
/** Default configuration values */
defaultConfig?: Record<string, unknown>;
/** Whether this node can be the starting point of a workflow */
isEntryPoint?: boolean;
/** Maximum number of this node type allowed in a workflow (undefined = unlimited) */
maxInstances?: number;
/** Documentation URL */
docsUrl?: string;
}
/**
* Registry of all node configurations by type
*/
export type NodeConfigRegistry = Record<string, NodeConfigMeta>;
/**
* Helper function to create a consistent port definition
*/
export function createPort(
name: string,
type: string,
options?: {
description?: string;
required?: boolean;
label?: I18nObject;
},
): ExtendedPortDefinition {
return {
name,
type,
description: options?.description,
required: options?.required ?? false,
label: options?.label,
};
}
/**
* Helper function to create input port
*/
export function createInput(
name: string,
type: string,
options?: {
description?: string;
required?: boolean;
label?: I18nObject;
},
): ExtendedPortDefinition {
return createPort(name, type, {
...options,
required: options?.required ?? true,
});
}
/**
* Helper function to create output port
*/
export function createOutput(
name: string,
type: string,
options?: {
description?: string;
label?: I18nObject;
},
): ExtendedPortDefinition {
return createPort(name, type, { ...options, required: false });
}

View File

@@ -38,7 +38,12 @@ import {
Play,
Plug,
ExternalLink,
BookOpen,
HardDrive,
Server,
Wrench,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import i18n from 'i18next';
import { resolveI18nLabel, maybeTranslateKey } from './workflow-i18n';
@@ -424,23 +429,56 @@ export function getNodeTypeLabel(
// ─── Dynamic Icon Resolution ────────────────────────────────────────
import * as LucideIcons from 'lucide-react';
/**
* Explicit icon registry mapping PascalCase icon names to their components.
*
* We use an explicit map instead of `import * as LucideIcons` because
* Next.js/webpack tree-shaking removes unused exports from barrel imports,
* causing dynamic lookups like `LucideIcons[name]` to fail at runtime.
*/
const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
ArrowRightLeft,
Bell,
BookOpen,
Bot,
Brain,
Clock,
Code,
Cpu,
Database,
ExternalLink,
FileText,
GitBranch,
GitMerge,
Globe,
HardDrive,
Layers,
ListFilter,
MessageCircle,
MessageSquare,
PauseCircle,
Play,
Plug,
Repeat,
Search,
Send,
Server,
Settings,
Split,
Timer,
Variable,
Webhook,
Workflow,
Wrench,
Zap,
};
/**
* Dynamically get Lucide icon component from backend icon name.
*
* This function enables the frontend to use icon names provided by the backend,
* eliminating the need to maintain a hardcoded NODE_ICONS mapping.
*
* @param iconName - Lucide icon name from backend (e.g., 'MessageSquare', 'Brain')
* @param nodeType - Node type for fallback to hardcoded mapping (backward compatibility)
* @returns React component for the icon
*
* @example
* ```tsx
* const Icon = getIconComponent('MessageSquare');
* return <Icon className="size-5" />;
* ```
*/
export function getIconComponent(
iconName: string | undefined,
@@ -448,24 +486,24 @@ export function getIconComponent(
): React.ElementType {
// 1. Priority: Use backend-provided icon name
if (iconName) {
const IconComponent = (LucideIcons as any)[iconName];
if (IconComponent && typeof IconComponent === 'function') {
const IconComponent = LUCIDE_ICON_REGISTRY[iconName];
if (IconComponent) {
return IconComponent;
}
// Warn if icon name is invalid
// Warn if icon name is not in registry
if (process.env.NODE_ENV === 'development') {
console.warn(
`[Workflow] Icon "${iconName}" not found in Lucide icons. ` +
`Falling back to default. Check: https://lucide.dev/icons/`
`[Workflow] Icon "${iconName}" not found in LUCIDE_ICON_REGISTRY. ` +
`Add it to workflow-constants.ts. Check: https://lucide.dev/icons/`
);
}
}
// 2. Fallback: Use hardcoded NODE_ICONS mapping (backward compatibility)
if (nodeType && NODE_ICONS[nodeType]) {
return NODE_ICONS[nodeType];
}
// 3. Final fallback: Default Settings icon
return LucideIcons.Settings;
return Settings;
}

View File

@@ -2,8 +2,8 @@
* Unified i18n utilities for the Workflow module.
*
* The backend API returns label dicts with keys like `zh-CN`, `en`,
* while node-configs use `zh_Hans`, `en_US`, and the i18next system
* uses `zh-Hans`, `en-US`. This module normalises **all** variants
* while some legacy persisted data may still use `zh_Hans`, `en_US`,
* and the i18next system uses `zh-Hans`, `en-US`. This module normalises **all** variants
* into a single lookup so every consumer gets the right value without
* maintaining its own fallback chain.
*/
@@ -26,7 +26,7 @@ const EN_KEYS = ['en-US', 'en_US', 'en'] as const;
* combination of `zh-CN`, `zh_Hans`, `en`, `en-US`, `en_US` etc.
*
* Works with both `Record<string, string>` (backend) and the typed
* `I18nObject` (node-configs).
* `I18nObject` used by legacy persisted workflow data.
*
* Optionally falls through to `i18n.t(value)` when the stored value
* itself looks like an i18n key (e.g. `"workflows.nodes.llmCall"`).

View File

@@ -2,8 +2,6 @@ import type {
WorkflowNodeTypeMetadata,
WorkflowPortDefinition,
} from '@/app/infra/entities/api';
import type { I18nObject } from '@/app/infra/entities/common';
import { getNodeConfig, type NodeConfigMeta } from './node-configs';
export const WORKFLOW_NODE_CATEGORIES = [
'trigger',
@@ -11,75 +9,35 @@ export const WORKFLOW_NODE_CATEGORIES = [
'control',
'action',
'integration',
'misc',
] as const;
const DEFAULT_INPUT_PORT: WorkflowPortDefinition = {
name: 'input',
type: 'any',
label: 'workflows.nodeInputs.input',
label: {
en_US: 'Input',
en: 'Input',
'en-US': 'Input',
zh_Hans: '输入',
'zh-Hans': '输入',
'zh-CN': '输入',
},
};
const DEFAULT_OUTPUT_PORT: WorkflowPortDefinition = {
name: 'output',
type: 'any',
label: 'workflows.nodeOutputs.output',
label: {
en_US: 'Output',
en: 'Output',
'en-US': 'Output',
zh_Hans: '输出',
'zh-Hans': '输出',
'zh-CN': '输出',
},
};
function ensurePortLabelKey(
prefix: 'workflows.nodeInputs' | 'workflows.nodeOutputs',
portName: string,
label?: string | Record<string, string>,
): string {
const key = `${prefix}.${portName}`;
if (typeof label === 'string') {
return label.startsWith(prefix) ? label : key;
}
if (label && typeof label === 'object') {
const existing = Object.values(label).find(
(value) => typeof value === 'string' && value.startsWith(prefix),
);
if (existing) return existing;
}
return key;
}
function normalizePort(
prefix: 'workflows.nodeInputs' | 'workflows.nodeOutputs',
port: WorkflowPortDefinition,
): WorkflowPortDefinition {
return {
...port,
label: ensurePortLabelKey(prefix, port.name, port.label),
};
}
function toBackendI18nObject(
value?: I18nObject,
): Record<string, string> | undefined {
if (!value) return undefined;
return {
'en-US': value.en_US,
en: value.en_US,
'zh-Hans': value.zh_Hans,
'zh-CN': value.zh_Hans,
};
}
function toWorkflowPortDefinition(
prefix: 'workflows.nodeInputs' | 'workflows.nodeOutputs',
port: NodeConfigMeta['inputs'][number] | NodeConfigMeta['outputs'][number],
): WorkflowPortDefinition {
return normalizePort(prefix, {
name: port.name,
type: port.type,
label: `${prefix}.${port.name}`,
});
}
function resolveNodeTypeCategory(type: string): string {
if (type.includes('.')) {
return type.split('.')[0];
@@ -87,47 +45,12 @@ function resolveNodeTypeCategory(type: string): string {
return 'process';
}
function getLocalConfigVariants(type: string): string[] {
const variants = new Set<string>([type]);
if (type.includes('.')) {
variants.add(type.split('.').slice(1).join('.'));
variants.add(type.replace(/\./g, '_'));
} else {
for (const category of WORKFLOW_NODE_CATEGORIES) {
variants.add(`${category}.${type}`);
}
}
return [...variants];
}
export function getLocalNodeTypeMeta(
type: string,
): WorkflowNodeTypeMetadata | null {
let localConfig: NodeConfigMeta | undefined;
for (const variant of getLocalConfigVariants(type)) {
localConfig = getNodeConfig(variant);
if (localConfig) break;
}
if (!localConfig) return null;
function normalizePort(
port: WorkflowPortDefinition,
): WorkflowPortDefinition {
return {
type,
category: localConfig.category,
label: toBackendI18nObject(localConfig.label) ?? {},
description: toBackendI18nObject(localConfig.description),
icon: localConfig.icon,
color: localConfig.color,
config_schema: localConfig.configSchema,
inputs: localConfig.inputs.map((input) =>
toWorkflowPortDefinition('workflows.nodeInputs', input),
),
outputs: localConfig.outputs.map((output) =>
toWorkflowPortDefinition('workflows.nodeOutputs', output),
),
...port,
type: port.type || 'any',
};
}
@@ -135,40 +58,24 @@ export function normalizeWorkflowNodeTypeMeta(
type: string,
nodeType?: WorkflowNodeTypeMetadata | null,
): WorkflowNodeTypeMetadata {
const localMeta = getLocalNodeTypeMeta(type);
const category =
nodeType?.category || localMeta?.category || resolveNodeTypeCategory(type);
const category = nodeType?.category || resolveNodeTypeCategory(type);
const inputs = nodeType?.inputs?.length
? nodeType.inputs.map((input) =>
normalizePort('workflows.nodeInputs', input),
)
: localMeta?.inputs?.length
? localMeta.inputs
: [DEFAULT_INPUT_PORT];
? nodeType.inputs.map(normalizePort)
: [DEFAULT_INPUT_PORT];
const outputs = nodeType?.outputs?.length
? nodeType.outputs.map((output) =>
normalizePort('workflows.nodeOutputs', output),
)
: localMeta?.outputs?.length
? localMeta.outputs
: [DEFAULT_OUTPUT_PORT];
const configSchema = nodeType?.config_schema?.length
? nodeType.config_schema
: localMeta?.config_schema?.length
? localMeta.config_schema
: [];
? nodeType.outputs.map(normalizePort)
: [DEFAULT_OUTPUT_PORT];
return {
type,
category,
label: nodeType?.label || localMeta?.label || {},
description: nodeType?.description || localMeta?.description,
icon: nodeType?.icon || localMeta?.icon,
color: nodeType?.color || localMeta?.color,
config_schema: configSchema,
label: nodeType?.label || {},
description: nodeType?.description,
icon: nodeType?.icon,
color: nodeType?.color,
config_schema: nodeType?.config_schema || [],
config_schema_source: nodeType?.config_schema_source,
config_stages: nodeType?.config_stages,
inputs,

View File

@@ -25,8 +25,8 @@ export interface WorkflowNode extends Node {
label: string;
type: string;
config: Record<string, unknown>;
inputs?: { name: string; label?: string; type?: string }[];
outputs?: { name: string; label?: string; type?: string }[];
inputs?: { name: string; label?: string | Record<string, string>; type?: string }[];
outputs?: { name: string; label?: string | Record<string, string>; type?: string }[];
};
}
@@ -270,9 +270,14 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
// Add new node
addNode: (type, position) => {
const { nodeTypes } = get();
const shortName = type.includes('.') ? type.split('.').pop()! : type;
const nodeType = normalizeWorkflowNodeTypeMeta(
type,
nodeTypes.find((t) => t.type === type),
nodeTypes.find((t) => {
if (t.type === type) return true;
const tShort = t.type.includes('.') ? t.type.split('.').pop()! : t.type;
return tShort === shortName;
}),
);
const getNodeLabel = (

View File

@@ -557,7 +557,7 @@ export interface WorkflowPosition {
export interface WorkflowPortDefinition {
name: string;
label?: string;
label?: string | Record<string, string>;
type?: string;
}