mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
backend
This commit is contained in:
@@ -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'))
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
283
src/langbot/pkg/workflow/metadata.py
Normal file
283
src/langbot/pkg/workflow/metadata.py
Normal 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}'
|
||||
@@ -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')
|
||||
|
||||
@@ -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]] = []
|
||||
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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}'}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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', {})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
|
||||
|
||||
@@ -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: []
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -6,5 +6,3 @@ export {
|
||||
export { default as NodePalette } from './NodePalette';
|
||||
export { default as PropertyPanel } from './PropertyPanel';
|
||||
|
||||
// Export node configurations
|
||||
export * from './node-configs';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"`).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -557,7 +557,7 @@ export interface WorkflowPosition {
|
||||
|
||||
export interface WorkflowPortDefinition {
|
||||
name: string;
|
||||
label?: string;
|
||||
label?: string | Record<string, string>;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user