From bb7db5344766b0c42e5424ad0b0fe8f4bb200807 Mon Sep 17 00:00:00 2001 From: Typer_Body Date: Mon, 18 May 2026 01:47:13 +0800 Subject: [PATCH] backend --- src/langbot/pkg/api/http/service/workflow.py | 40 +- src/langbot/pkg/core/stages/load_config.py | 80 +- src/langbot/pkg/workflow/__init__.py | 33 +- src/langbot/pkg/workflow/metadata.py | 283 +++++ src/langbot/pkg/workflow/node.py | 271 ++-- .../pkg/workflow/nodes/call_pipeline.py | 19 +- .../pkg/workflow/nodes/code_executor.py | 17 +- src/langbot/pkg/workflow/nodes/condition.py | 17 +- src/langbot/pkg/workflow/nodes/coze_bot.py | 17 +- .../pkg/workflow/nodes/cron_trigger.py | 17 +- .../pkg/workflow/nodes/data_transform.py | 17 +- .../pkg/workflow/nodes/database_query.py | 17 +- .../workflow/nodes/dify_knowledge_query.py | 17 +- .../pkg/workflow/nodes/dify_workflow.py | 17 +- src/langbot/pkg/workflow/nodes/end.py | 19 +- .../pkg/workflow/nodes/event_trigger.py | 17 +- .../pkg/workflow/nodes/http_request.py | 19 +- src/langbot/pkg/workflow/nodes/iterator.py | 35 +- .../pkg/workflow/nodes/knowledge_retrieval.py | 17 +- .../pkg/workflow/nodes/langflow_flow.py | 17 +- src/langbot/pkg/workflow/nodes/llm_call.py | 65 +- src/langbot/pkg/workflow/nodes/loop.py | 42 +- src/langbot/pkg/workflow/nodes/mcp_tool.py | 16 +- .../pkg/workflow/nodes/memory_store.py | 18 +- src/langbot/pkg/workflow/nodes/merge.py | 17 +- .../pkg/workflow/nodes/message_trigger.py | 17 +- .../pkg/workflow/nodes/n8n_workflow.py | 17 +- .../pkg/workflow/nodes/opening_statement.py | 17 +- src/langbot/pkg/workflow/nodes/parallel.py | 39 +- .../pkg/workflow/nodes/parameter_extractor.py | 16 +- src/langbot/pkg/workflow/nodes/plugin_call.py | 5 +- .../pkg/workflow/nodes/question_classifier.py | 17 +- .../pkg/workflow/nodes/redis_operation.py | 17 +- .../pkg/workflow/nodes/reply_message.py | 17 +- .../pkg/workflow/nodes/send_message.py | 17 +- .../pkg/workflow/nodes/set_variable.py | 17 +- src/langbot/pkg/workflow/nodes/store_data.py | 17 +- src/langbot/pkg/workflow/nodes/switch.py | 17 +- .../pkg/workflow/nodes/variable_aggregator.py | 17 +- src/langbot/pkg/workflow/nodes/wait.py | 17 +- .../pkg/workflow/nodes/webhook_trigger.py | 17 +- src/langbot/pkg/workflow/registry.py | 429 +++++-- .../metadata/nodes/call_pipeline.yaml | 9 +- .../metadata/nodes/code_executor.yaml | 9 +- .../templates/metadata/nodes/condition.yaml | 9 +- .../metadata/nodes/cron_trigger.yaml | 9 +- .../metadata/nodes/data_transform.yaml | 9 +- .../metadata/nodes/database_query.yaml | 9 +- src/langbot/templates/metadata/nodes/end.yaml | 9 +- .../metadata/nodes/event_trigger.yaml | 9 +- .../metadata/nodes/http_request.yaml | 9 +- .../templates/metadata/nodes/iterator.yaml | 9 +- .../metadata/nodes/knowledge_retrieval.yaml | 15 +- .../templates/metadata/nodes/llm_call.yaml | 9 +- .../templates/metadata/nodes/loop.yaml | 9 +- .../templates/metadata/nodes/mcp_tool.yaml | 9 +- .../metadata/nodes/memory_store.yaml | 9 +- .../templates/metadata/nodes/merge.yaml | 9 +- .../metadata/nodes/message_trigger.yaml | 9 +- .../metadata/nodes/opening_statement.yaml | 9 +- .../templates/metadata/nodes/parallel.yaml | 9 +- .../metadata/nodes/parameter_extractor.yaml | 9 +- .../metadata/nodes/question_classifier.yaml | 9 +- .../metadata/nodes/redis_operation.yaml | 9 +- .../metadata/nodes/reply_message.yaml | 9 +- .../metadata/nodes/send_message.yaml | 9 +- .../metadata/nodes/set_variable.yaml | 9 +- .../templates/metadata/nodes/store_data.yaml | 9 +- .../templates/metadata/nodes/switch.yaml | 9 +- .../metadata/nodes/variable_aggregator.yaml | 9 +- .../templates/metadata/nodes/wait.yaml | 9 +- .../metadata/nodes/webhook_trigger.yaml | 9 +- .../home/workflows/WorkflowDetailContent.tsx | 22 +- .../workflow-editor/NodePalette.tsx | 48 +- .../workflow-editor/PropertyPanel.tsx | 68 +- .../components/workflow-editor/index.ts | 2 - .../node-configs/action-configs.ts | 1125 ----------------- .../node-configs/ai-configs.ts | 774 ------------ .../node-configs/control-configs.ts | 998 --------------- .../workflow-editor/node-configs/index.ts | 261 ---- .../node-configs/integration-configs.ts | 912 ------------- .../node-configs/process-configs.ts | 833 ------------ .../node-configs/trigger-configs.ts | 542 -------- .../workflow-editor/node-configs/types.ts | 120 -- .../workflow-editor/workflow-constants.ts | 74 +- .../workflow-editor/workflow-i18n.ts | 6 +- .../workflow-editor/workflow-node-metadata.ts | 157 +-- .../home/workflows/store/useWorkflowStore.ts | 11 +- web/src/app/infra/entities/api/index.ts | 2 +- 89 files changed, 1202 insertions(+), 6883 deletions(-) create mode 100644 src/langbot/pkg/workflow/metadata.py delete mode 100644 web/src/app/home/workflows/components/workflow-editor/node-configs/action-configs.ts delete mode 100644 web/src/app/home/workflows/components/workflow-editor/node-configs/ai-configs.ts delete mode 100644 web/src/app/home/workflows/components/workflow-editor/node-configs/control-configs.ts delete mode 100644 web/src/app/home/workflows/components/workflow-editor/node-configs/index.ts delete mode 100644 web/src/app/home/workflows/components/workflow-editor/node-configs/integration-configs.ts delete mode 100644 web/src/app/home/workflows/components/workflow-editor/node-configs/process-configs.ts delete mode 100644 web/src/app/home/workflows/components/workflow-editor/node-configs/trigger-configs.ts delete mode 100644 web/src/app/home/workflows/components/workflow-editor/node-configs/types.ts diff --git a/src/langbot/pkg/api/http/service/workflow.py b/src/langbot/pkg/api/http/service/workflow.py index 418615f5..34b2aab0 100644 --- a/src/langbot/pkg/api/http/service/workflow.py +++ b/src/langbot/pkg/api/http/service/workflow.py @@ -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')) diff --git a/src/langbot/pkg/core/stages/load_config.py b/src/langbot/pkg/core/stages/load_config.py index e9a61b53..2250f294 100644 --- a/src/langbot/pkg/core/stages/load_config.py +++ b/src/langbot/pkg/core/stages/load_config.py @@ -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' + ) diff --git a/src/langbot/pkg/workflow/__init__.py b/src/langbot/pkg/workflow/__init__.py index 3c512917..cd634feb 100644 --- a/src/langbot/pkg/workflow/__init__.py +++ b/src/langbot/pkg/workflow/__init__.py @@ -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', diff --git a/src/langbot/pkg/workflow/metadata.py b/src/langbot/pkg/workflow/metadata.py new file mode 100644 index 00000000..b63ded8d --- /dev/null +++ b/src/langbot/pkg/workflow/metadata.py @@ -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}' diff --git a/src/langbot/pkg/workflow/node.py b/src/langbot/pkg/workflow/node.py index c4a90619..8562f69c 100644 --- a/src/langbot/pkg/workflow/node.py +++ b/src/langbot/pkg/workflow/node.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/call_pipeline.py b/src/langbot/pkg/workflow/nodes/call_pipeline.py index b00f1c1d..6c1684df 100644 --- a/src/langbot/pkg/workflow/nodes/call_pipeline.py +++ b/src/langbot/pkg/workflow/nodes/call_pipeline.py @@ -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]] = [] diff --git a/src/langbot/pkg/workflow/nodes/code_executor.py b/src/langbot/pkg/workflow/nodes/code_executor.py index 24d1b46e..af27998d 100644 --- a/src/langbot/pkg/workflow/nodes/code_executor.py +++ b/src/langbot/pkg/workflow/nodes/code_executor.py @@ -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', '') diff --git a/src/langbot/pkg/workflow/nodes/condition.py b/src/langbot/pkg/workflow/nodes/condition.py index cfa85f3c..e50478a8 100644 --- a/src/langbot/pkg/workflow/nodes/condition.py +++ b/src/langbot/pkg/workflow/nodes/condition.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/coze_bot.py b/src/langbot/pkg/workflow/nodes/coze_bot.py index b71b0dca..e086f157 100644 --- a/src/langbot/pkg/workflow/nodes/coze_bot.py +++ b/src/langbot/pkg/workflow/nodes/coze_bot.py @@ -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', '') diff --git a/src/langbot/pkg/workflow/nodes/cron_trigger.py b/src/langbot/pkg/workflow/nodes/cron_trigger.py index d84140cf..6a480d3c 100644 --- a/src/langbot/pkg/workflow/nodes/cron_trigger.py +++ b/src/langbot/pkg/workflow/nodes/cron_trigger.py @@ -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 diff --git a/src/langbot/pkg/workflow/nodes/data_transform.py b/src/langbot/pkg/workflow/nodes/data_transform.py index aac727b0..c5eec5d2 100644 --- a/src/langbot/pkg/workflow/nodes/data_transform.py +++ b/src/langbot/pkg/workflow/nodes/data_transform.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/database_query.py b/src/langbot/pkg/workflow/nodes/database_query.py index 3c3f41ce..22c7e9d6 100644 --- a/src/langbot/pkg/workflow/nodes/database_query.py +++ b/src/langbot/pkg/workflow/nodes/database_query.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/dify_knowledge_query.py b/src/langbot/pkg/workflow/nodes/dify_knowledge_query.py index ed6285e0..07f35c04 100644 --- a/src/langbot/pkg/workflow/nodes/dify_knowledge_query.py +++ b/src/langbot/pkg/workflow/nodes/dify_knowledge_query.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/dify_workflow.py b/src/langbot/pkg/workflow/nodes/dify_workflow.py index 76513159..13477905 100644 --- a/src/langbot/pkg/workflow/nodes/dify_workflow.py +++ b/src/langbot/pkg/workflow/nodes/dify_workflow.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/end.py b/src/langbot/pkg/workflow/nodes/end.py index add94676..2fdc8713 100644 --- a/src/langbot/pkg/workflow/nodes/end.py +++ b/src/langbot/pkg/workflow/nodes/end.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/event_trigger.py b/src/langbot/pkg/workflow/nodes/event_trigger.py index 2a20e50c..2bb5004b 100644 --- a/src/langbot/pkg/workflow/nodes/event_trigger.py +++ b/src/langbot/pkg/workflow/nodes/event_trigger.py @@ -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 diff --git a/src/langbot/pkg/workflow/nodes/http_request.py b/src/langbot/pkg/workflow/nodes/http_request.py index 42c99a51..004dfb36 100644 --- a/src/langbot/pkg/workflow/nodes/http_request.py +++ b/src/langbot/pkg/workflow/nodes/http_request.py @@ -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 diff --git a/src/langbot/pkg/workflow/nodes/iterator.py b/src/langbot/pkg/workflow/nodes/iterator.py index 6007ab3e..5bd7526c 100644 --- a/src/langbot/pkg/workflow/nodes/iterator.py +++ b/src/langbot/pkg/workflow/nodes/iterator.py @@ -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', []) diff --git a/src/langbot/pkg/workflow/nodes/knowledge_retrieval.py b/src/langbot/pkg/workflow/nodes/knowledge_retrieval.py index 9d6f9aee..1cd14f85 100644 --- a/src/langbot/pkg/workflow/nodes/knowledge_retrieval.py +++ b/src/langbot/pkg/workflow/nodes/knowledge_retrieval.py @@ -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', '') diff --git a/src/langbot/pkg/workflow/nodes/langflow_flow.py b/src/langbot/pkg/workflow/nodes/langflow_flow.py index 37778b0c..9edc8290 100644 --- a/src/langbot/pkg/workflow/nodes/langflow_flow.py +++ b/src/langbot/pkg/workflow/nodes/langflow_flow.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/llm_call.py b/src/langbot/pkg/workflow/nodes/llm_call.py index 22b80cba..c3db25b1 100644 --- a/src/langbot/pkg/workflow/nodes/llm_call.py +++ b/src/langbot/pkg/workflow/nodes/llm_call.py @@ -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.""" diff --git a/src/langbot/pkg/workflow/nodes/loop.py b/src/langbot/pkg/workflow/nodes/loop.py index 92e82346..f33ed72a 100644 --- a/src/langbot/pkg/workflow/nodes/loop.py +++ b/src/langbot/pkg/workflow/nodes/loop.py @@ -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', []) diff --git a/src/langbot/pkg/workflow/nodes/mcp_tool.py b/src/langbot/pkg/workflow/nodes/mcp_tool.py index af571004..60e22734 100644 --- a/src/langbot/pkg/workflow/nodes/mcp_tool.py +++ b/src/langbot/pkg/workflow/nodes/mcp_tool.py @@ -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 diff --git a/src/langbot/pkg/workflow/nodes/memory_store.py b/src/langbot/pkg/workflow/nodes/memory_store.py index c93cf402..35edc658 100644 --- a/src/langbot/pkg/workflow/nodes/memory_store.py +++ b/src/langbot/pkg/workflow/nodes/memory_store.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/merge.py b/src/langbot/pkg/workflow/nodes/merge.py index 4e5290c1..83b677a3 100644 --- a/src/langbot/pkg/workflow/nodes/merge.py +++ b/src/langbot/pkg/workflow/nodes/merge.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/message_trigger.py b/src/langbot/pkg/workflow/nodes/message_trigger.py index 74db03e4..ee07cc2c 100644 --- a/src/langbot/pkg/workflow/nodes/message_trigger.py +++ b/src/langbot/pkg/workflow/nodes/message_trigger.py @@ -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 diff --git a/src/langbot/pkg/workflow/nodes/n8n_workflow.py b/src/langbot/pkg/workflow/nodes/n8n_workflow.py index 61e32799..e7c5878b 100644 --- a/src/langbot/pkg/workflow/nodes/n8n_workflow.py +++ b/src/langbot/pkg/workflow/nodes/n8n_workflow.py @@ -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', '') diff --git a/src/langbot/pkg/workflow/nodes/opening_statement.py b/src/langbot/pkg/workflow/nodes/opening_statement.py index 428682e3..2cb8b30b 100644 --- a/src/langbot/pkg/workflow/nodes/opening_statement.py +++ b/src/langbot/pkg/workflow/nodes/opening_statement.py @@ -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', '') diff --git a/src/langbot/pkg/workflow/nodes/parallel.py b/src/langbot/pkg/workflow/nodes/parallel.py index 73c8028a..3eb970ff 100644 --- a/src/langbot/pkg/workflow/nodes/parallel.py +++ b/src/langbot/pkg/workflow/nodes/parallel.py @@ -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 { diff --git a/src/langbot/pkg/workflow/nodes/parameter_extractor.py b/src/langbot/pkg/workflow/nodes/parameter_extractor.py index 2f262238..4b383a58 100644 --- a/src/langbot/pkg/workflow/nodes/parameter_extractor.py +++ b/src/langbot/pkg/workflow/nodes/parameter_extractor.py @@ -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', []) diff --git a/src/langbot/pkg/workflow/nodes/plugin_call.py b/src/langbot/pkg/workflow/nodes/plugin_call.py index e80ce83a..a420c60a 100644 --- a/src/langbot/pkg/workflow/nodes/plugin_call.py +++ b/src/langbot/pkg/workflow/nodes/plugin_call.py @@ -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): diff --git a/src/langbot/pkg/workflow/nodes/question_classifier.py b/src/langbot/pkg/workflow/nodes/question_classifier.py index d7cf70e1..fc3fe6f8 100644 --- a/src/langbot/pkg/workflow/nodes/question_classifier.py +++ b/src/langbot/pkg/workflow/nodes/question_classifier.py @@ -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', []) diff --git a/src/langbot/pkg/workflow/nodes/redis_operation.py b/src/langbot/pkg/workflow/nodes/redis_operation.py index 21af7967..8c74510f 100644 --- a/src/langbot/pkg/workflow/nodes/redis_operation.py +++ b/src/langbot/pkg/workflow/nodes/redis_operation.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/reply_message.py b/src/langbot/pkg/workflow/nodes/reply_message.py index 03895132..223fbc20 100644 --- a/src/langbot/pkg/workflow/nodes/reply_message.py +++ b/src/langbot/pkg/workflow/nodes/reply_message.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/send_message.py b/src/langbot/pkg/workflow/nodes/send_message.py index 286b983a..3c2237a1 100644 --- a/src/langbot/pkg/workflow/nodes/send_message.py +++ b/src/langbot/pkg/workflow/nodes/send_message.py @@ -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}'} diff --git a/src/langbot/pkg/workflow/nodes/set_variable.py b/src/langbot/pkg/workflow/nodes/set_variable.py index 14246d6c..50031656 100644 --- a/src/langbot/pkg/workflow/nodes/set_variable.py +++ b/src/langbot/pkg/workflow/nodes/set_variable.py @@ -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') diff --git a/src/langbot/pkg/workflow/nodes/store_data.py b/src/langbot/pkg/workflow/nodes/store_data.py index 62ffeeec..01d1ff90 100644 --- a/src/langbot/pkg/workflow/nodes/store_data.py +++ b/src/langbot/pkg/workflow/nodes/store_data.py @@ -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', '') diff --git a/src/langbot/pkg/workflow/nodes/switch.py b/src/langbot/pkg/workflow/nodes/switch.py index 0c8474ea..22f0c674 100644 --- a/src/langbot/pkg/workflow/nodes/switch.py +++ b/src/langbot/pkg/workflow/nodes/switch.py @@ -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', '') diff --git a/src/langbot/pkg/workflow/nodes/variable_aggregator.py b/src/langbot/pkg/workflow/nodes/variable_aggregator.py index 547946f8..5b5fe821 100644 --- a/src/langbot/pkg/workflow/nodes/variable_aggregator.py +++ b/src/langbot/pkg/workflow/nodes/variable_aggregator.py @@ -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', {}) diff --git a/src/langbot/pkg/workflow/nodes/wait.py b/src/langbot/pkg/workflow/nodes/wait.py index 6a5e8d20..08844087 100644 --- a/src/langbot/pkg/workflow/nodes/wait.py +++ b/src/langbot/pkg/workflow/nodes/wait.py @@ -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 diff --git a/src/langbot/pkg/workflow/nodes/webhook_trigger.py b/src/langbot/pkg/workflow/nodes/webhook_trigger.py index 95ef183c..af95c598 100644 --- a/src/langbot/pkg/workflow/nodes/webhook_trigger.py +++ b/src/langbot/pkg/workflow/nodes/webhook_trigger.py @@ -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 diff --git a/src/langbot/pkg/workflow/registry.py b/src/langbot/pkg/workflow/registry.py index 7ee364df..37fae180 100644 --- a/src/langbot/pkg/workflow/registry.py +++ b/src/langbot/pkg/workflow/registry.py @@ -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() diff --git a/src/langbot/templates/metadata/nodes/call_pipeline.yaml b/src/langbot/templates/metadata/nodes/call_pipeline.yaml index 5f25cae6..b96bffa0 100644 --- a/src/langbot/templates/metadata/nodes/call_pipeline.yaml +++ b/src/langbot/templates/metadata/nodes/call_pipeline.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/code_executor.yaml b/src/langbot/templates/metadata/nodes/code_executor.yaml index d3225c42..9b25f902 100644 --- a/src/langbot/templates/metadata/nodes/code_executor.yaml +++ b/src/langbot/templates/metadata/nodes/code_executor.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/condition.yaml b/src/langbot/templates/metadata/nodes/condition.yaml index 17c4ad4f..b13c8ede 100644 --- a/src/langbot/templates/metadata/nodes/condition.yaml +++ b/src/langbot/templates/metadata/nodes/condition.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/cron_trigger.yaml b/src/langbot/templates/metadata/nodes/cron_trigger.yaml index 88692112..650f85a4 100644 --- a/src/langbot/templates/metadata/nodes/cron_trigger.yaml +++ b/src/langbot/templates/metadata/nodes/cron_trigger.yaml @@ -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: [] diff --git a/src/langbot/templates/metadata/nodes/data_transform.yaml b/src/langbot/templates/metadata/nodes/data_transform.yaml index b82bf9d3..f18881f5 100644 --- a/src/langbot/templates/metadata/nodes/data_transform.yaml +++ b/src/langbot/templates/metadata/nodes/data_transform.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/database_query.yaml b/src/langbot/templates/metadata/nodes/database_query.yaml index 1c51b8d6..3f24d376 100644 --- a/src/langbot/templates/metadata/nodes/database_query.yaml +++ b/src/langbot/templates/metadata/nodes/database_query.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/end.yaml b/src/langbot/templates/metadata/nodes/end.yaml index eba85546..05a930e6 100644 --- a/src/langbot/templates/metadata/nodes/end.yaml +++ b/src/langbot/templates/metadata/nodes/end.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/event_trigger.yaml b/src/langbot/templates/metadata/nodes/event_trigger.yaml index b50db89c..d294af3f 100644 --- a/src/langbot/templates/metadata/nodes/event_trigger.yaml +++ b/src/langbot/templates/metadata/nodes/event_trigger.yaml @@ -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: [] diff --git a/src/langbot/templates/metadata/nodes/http_request.yaml b/src/langbot/templates/metadata/nodes/http_request.yaml index 96cdec15..5ba8b910 100644 --- a/src/langbot/templates/metadata/nodes/http_request.yaml +++ b/src/langbot/templates/metadata/nodes/http_request.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/iterator.yaml b/src/langbot/templates/metadata/nodes/iterator.yaml index 635367af..b92d07b9 100644 --- a/src/langbot/templates/metadata/nodes/iterator.yaml +++ b/src/langbot/templates/metadata/nodes/iterator.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/knowledge_retrieval.yaml b/src/langbot/templates/metadata/nodes/knowledge_retrieval.yaml index 645ca183..41b188c0 100644 --- a/src/langbot/templates/metadata/nodes/knowledge_retrieval.yaml +++ b/src/langbot/templates/metadata/nodes/knowledge_retrieval.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/llm_call.yaml b/src/langbot/templates/metadata/nodes/llm_call.yaml index bc5752ac..418a4ad7 100644 --- a/src/langbot/templates/metadata/nodes/llm_call.yaml +++ b/src/langbot/templates/metadata/nodes/llm_call.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/loop.yaml b/src/langbot/templates/metadata/nodes/loop.yaml index c3d6e97f..64d0599d 100644 --- a/src/langbot/templates/metadata/nodes/loop.yaml +++ b/src/langbot/templates/metadata/nodes/loop.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/mcp_tool.yaml b/src/langbot/templates/metadata/nodes/mcp_tool.yaml index bbc29065..a6bc007b 100644 --- a/src/langbot/templates/metadata/nodes/mcp_tool.yaml +++ b/src/langbot/templates/metadata/nodes/mcp_tool.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/memory_store.yaml b/src/langbot/templates/metadata/nodes/memory_store.yaml index c968221f..f5b13071 100644 --- a/src/langbot/templates/metadata/nodes/memory_store.yaml +++ b/src/langbot/templates/metadata/nodes/memory_store.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/merge.yaml b/src/langbot/templates/metadata/nodes/merge.yaml index cef1b0ce..9832573f 100644 --- a/src/langbot/templates/metadata/nodes/merge.yaml +++ b/src/langbot/templates/metadata/nodes/merge.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/message_trigger.yaml b/src/langbot/templates/metadata/nodes/message_trigger.yaml index a2c84c83..ea2f8638 100644 --- a/src/langbot/templates/metadata/nodes/message_trigger.yaml +++ b/src/langbot/templates/metadata/nodes/message_trigger.yaml @@ -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: [] diff --git a/src/langbot/templates/metadata/nodes/opening_statement.yaml b/src/langbot/templates/metadata/nodes/opening_statement.yaml index 2626ac27..a9cd2ace 100644 --- a/src/langbot/templates/metadata/nodes/opening_statement.yaml +++ b/src/langbot/templates/metadata/nodes/opening_statement.yaml @@ -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: [] diff --git a/src/langbot/templates/metadata/nodes/parallel.yaml b/src/langbot/templates/metadata/nodes/parallel.yaml index 44d8c7b4..b88fcda7 100644 --- a/src/langbot/templates/metadata/nodes/parallel.yaml +++ b/src/langbot/templates/metadata/nodes/parallel.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/parameter_extractor.yaml b/src/langbot/templates/metadata/nodes/parameter_extractor.yaml index 04595dc0..7b9ba4bc 100644 --- a/src/langbot/templates/metadata/nodes/parameter_extractor.yaml +++ b/src/langbot/templates/metadata/nodes/parameter_extractor.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/question_classifier.yaml b/src/langbot/templates/metadata/nodes/question_classifier.yaml index c64c7395..ed0a6b7f 100644 --- a/src/langbot/templates/metadata/nodes/question_classifier.yaml +++ b/src/langbot/templates/metadata/nodes/question_classifier.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/redis_operation.yaml b/src/langbot/templates/metadata/nodes/redis_operation.yaml index de7b73f3..4790dc1d 100644 --- a/src/langbot/templates/metadata/nodes/redis_operation.yaml +++ b/src/langbot/templates/metadata/nodes/redis_operation.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/reply_message.yaml b/src/langbot/templates/metadata/nodes/reply_message.yaml index 4d190d56..ccda2af2 100644 --- a/src/langbot/templates/metadata/nodes/reply_message.yaml +++ b/src/langbot/templates/metadata/nodes/reply_message.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/send_message.yaml b/src/langbot/templates/metadata/nodes/send_message.yaml index 5995dd8b..5936e5d0 100644 --- a/src/langbot/templates/metadata/nodes/send_message.yaml +++ b/src/langbot/templates/metadata/nodes/send_message.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/set_variable.yaml b/src/langbot/templates/metadata/nodes/set_variable.yaml index 604decb9..07e656df 100644 --- a/src/langbot/templates/metadata/nodes/set_variable.yaml +++ b/src/langbot/templates/metadata/nodes/set_variable.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/store_data.yaml b/src/langbot/templates/metadata/nodes/store_data.yaml index 8f3cda35..b95941c0 100644 --- a/src/langbot/templates/metadata/nodes/store_data.yaml +++ b/src/langbot/templates/metadata/nodes/store_data.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/switch.yaml b/src/langbot/templates/metadata/nodes/switch.yaml index f2bb823a..37edabda 100644 --- a/src/langbot/templates/metadata/nodes/switch.yaml +++ b/src/langbot/templates/metadata/nodes/switch.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/variable_aggregator.yaml b/src/langbot/templates/metadata/nodes/variable_aggregator.yaml index 07873dcf..6cbea1ae 100644 --- a/src/langbot/templates/metadata/nodes/variable_aggregator.yaml +++ b/src/langbot/templates/metadata/nodes/variable_aggregator.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/wait.yaml b/src/langbot/templates/metadata/nodes/wait.yaml index a06a65c4..a402d49c 100644 --- a/src/langbot/templates/metadata/nodes/wait.yaml +++ b/src/langbot/templates/metadata/nodes/wait.yaml @@ -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 diff --git a/src/langbot/templates/metadata/nodes/webhook_trigger.yaml b/src/langbot/templates/metadata/nodes/webhook_trigger.yaml index e2a268ca..38f9c1be 100644 --- a/src/langbot/templates/metadata/nodes/webhook_trigger.yaml +++ b/src/langbot/templates/metadata/nodes/webhook_trigger.yaml @@ -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: [] diff --git a/web/src/app/home/workflows/WorkflowDetailContent.tsx b/web/src/app/home/workflows/WorkflowDetailContent.tsx index 0933fd66..8d2d6e0f 100644 --- a/web/src/app/home/workflows/WorkflowDetailContent.tsx +++ b/web/src/app/home/workflows/WorkflowDetailContent.tsx @@ -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(() => { diff --git a/web/src/app/home/workflows/components/workflow-editor/NodePalette.tsx b/web/src/app/home/workflows/components/workflow-editor/NodePalette.tsx index 7abd8973..58be07e1 100644 --- a/web/src/app/home/workflows/components/workflow-editor/NodePalette.tsx +++ b/web/src/app/home/workflows/components/workflow-editor/NodePalette.tsx @@ -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; } -// 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 diff --git a/web/src/app/home/workflows/components/workflow-editor/PropertyPanel.tsx b/web/src/app/home/workflows/components/workflow-editor/PropertyPanel.tsx index 57edb838..a3aefd64 100644 --- a/web/src/app/home/workflows/components/workflow-editor/PropertyPanel.tsx +++ b/web/src/app/home/workflows/components/workflow-editor/PropertyPanel.tsx @@ -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 (
diff --git a/web/src/app/home/workflows/components/workflow-editor/index.ts b/web/src/app/home/workflows/components/workflow-editor/index.ts index a0fd7789..5b1abcb7 100644 --- a/web/src/app/home/workflows/components/workflow-editor/index.ts +++ b/web/src/app/home/workflows/components/workflow-editor/index.ts @@ -6,5 +6,3 @@ export { export { default as NodePalette } from './NodePalette'; export { default as PropertyPanel } from './PropertyPanel'; -// Export node configurations -export * from './node-configs'; diff --git a/web/src/app/home/workflows/components/workflow-editor/node-configs/action-configs.ts b/web/src/app/home/workflows/components/workflow-editor/node-configs/action-configs.ts deleted file mode 100644 index d1af3c39..00000000 --- a/web/src/app/home/workflows/components/workflow-editor/node-configs/action-configs.ts +++ /dev/null @@ -1,1125 +0,0 @@ -/** - * Action Node Configurations - * - * Defines configurations for action node types: - * - send_message: Send a message - * - http_request: Make HTTP requests - * - bot_invoke: Invoke another bot - * - workflow_invoke: Invoke another workflow - * - notification: Send notifications - */ - -import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic'; -import { NodeConfigMeta, createInput, createOutput } from './types'; - -/** - * Send Message Node - * Sends a message to a chat or user - */ -export const sendMessageConfig: NodeConfigMeta = { - nodeType: 'send_message', - label: { - en_US: 'Send Message', - zh_Hans: '发送消息', - }, - description: { - en_US: 'Send a message to a chat or user', - zh_Hans: '向聊天或用户发送消息', - }, - icon: 'Send', - category: 'action', - color: '#10b981', - inputs: [ - createInput('content', 'string', { - description: 'Message content to send', - label: { en_US: 'Content', zh_Hans: '内容' }, - required: false, - }), - createInput('context', 'object', { - description: 'Message context (for reply)', - label: { en_US: 'Context', zh_Hans: '上下文' }, - required: false, - }), - ], - outputs: [ - createOutput('message_id', 'string', { - description: 'ID of the sent message', - label: { en_US: 'Message ID', zh_Hans: '消息 ID' }, - }), - createOutput('success', 'boolean', { - description: 'Whether the message was sent successfully', - label: { en_US: 'Success', zh_Hans: '成功' }, - }), - ], - configSchema: [ - { - id: 'message_type', - name: 'message_type', - type: DynamicFormItemType.SELECT, - label: { - en_US: 'Message Type', - zh_Hans: '消息类型', - }, - description: { - en_US: 'Type of message to send', - zh_Hans: '要发送的消息类型', - }, - required: true, - default: 'text', - options: [ - { name: 'text', label: { en_US: 'Text', zh_Hans: '文本' } }, - { - name: 'markdown', - label: { en_US: 'Markdown', zh_Hans: 'Markdown 文本' }, - }, - { name: 'image', label: { en_US: 'Image', zh_Hans: '图片' } }, - { name: 'file', label: { en_US: 'File', zh_Hans: '文件' } }, - { name: 'card', label: { en_US: 'Card', zh_Hans: '卡片' } }, - ], - }, - { - id: 'content_template', - name: 'content_template', - type: DynamicFormItemType.TEXT, - label: { - en_US: 'Content Template', - zh_Hans: '内容模板', - }, - description: { - en_US: - 'Message content template (supports variables). Leave empty to use input.', - zh_Hans: '消息内容模板(支持变量)。留空则使用输入。', - }, - required: false, - default: '', - }, - { - id: 'reply_to_original', - name: 'reply_to_original', - type: DynamicFormItemType.BOOLEAN, - label: { - en_US: 'Reply to Original', - zh_Hans: '回复原消息', - }, - description: { - en_US: 'Reply to the original message that triggered the workflow', - zh_Hans: '回复触发工作流的原始消息', - }, - required: false, - default: true, - }, - { - id: 'at_sender', - name: 'at_sender', - type: DynamicFormItemType.BOOLEAN, - label: { - en_US: '@ Sender', - zh_Hans: '@ 发送者', - }, - description: { - en_US: 'Mention the original sender in the reply', - zh_Hans: '在回复中提及原始发送者', - }, - required: false, - default: false, - }, - ], - defaultConfig: { - message_type: 'text', - content_template: '', - reply_to_original: true, - at_sender: false, - }, -}; - -/** - * HTTP Request Node - * Makes HTTP requests to external APIs - */ -export const httpRequestConfig: NodeConfigMeta = { - nodeType: 'http_request', - label: { - en_US: 'HTTP Request', - zh_Hans: 'HTTP 请求', - }, - description: { - en_US: 'Make HTTP requests to external APIs', - zh_Hans: '向外部 API 发送 HTTP 请求', - }, - icon: 'Globe', - category: 'action', - color: '#10b981', - inputs: [ - createInput('body', 'any', { - description: 'Request body data', - label: { en_US: 'Body', zh_Hans: '请求体' }, - required: false, - }), - createInput('variables', 'object', { - description: 'Variables for URL/header templates', - label: { en_US: 'Variables', zh_Hans: '变量' }, - required: false, - }), - ], - outputs: [ - createOutput('response', 'any', { - description: 'Response body', - label: { en_US: 'Response', zh_Hans: '响应' }, - }), - createOutput('status_code', 'number', { - description: 'HTTP status code', - label: { en_US: 'Status Code', zh_Hans: '状态码' }, - }), - createOutput('headers', 'object', { - description: 'Response headers', - label: { en_US: 'Headers', zh_Hans: '响应头' }, - }), - createOutput('success', 'boolean', { - description: 'Whether request was successful (2xx)', - label: { en_US: 'Success', zh_Hans: '成功' }, - }), - ], - configSchema: [ - { - id: 'method', - name: 'method', - type: DynamicFormItemType.SELECT, - label: { - en_US: 'Method', - zh_Hans: '方法', - }, - description: { - en_US: 'HTTP method', - zh_Hans: 'HTTP 方法', - }, - required: true, - default: 'GET', - options: [ - { name: 'GET', label: { en_US: 'GET', zh_Hans: 'GET' } }, - { name: 'POST', label: { en_US: 'POST', zh_Hans: 'POST' } }, - { name: 'PUT', label: { en_US: 'PUT', zh_Hans: 'PUT' } }, - { name: 'PATCH', label: { en_US: 'PATCH', zh_Hans: 'PATCH' } }, - { name: 'DELETE', label: { en_US: 'DELETE', zh_Hans: 'DELETE' } }, - ], - }, - { - id: 'url', - name: 'url', - type: DynamicFormItemType.STRING, - label: { - en_US: 'URL', - zh_Hans: 'URL', - }, - description: { - en_US: 'Request URL (supports variable interpolation)', - zh_Hans: '请求 URL(支持变量插值)', - }, - required: true, - default: '', - }, - { - id: 'headers', - name: 'headers', - type: DynamicFormItemType.TEXT, - label: { - en_US: 'Headers', - zh_Hans: '请求头', - }, - description: { - en_US: 'Request headers as JSON object', - zh_Hans: '请求头(JSON 对象格式)', - }, - required: false, - default: '{}', - }, - { - id: 'body_type', - name: 'body_type', - type: DynamicFormItemType.SELECT, - label: { - en_US: 'Body Type', - zh_Hans: '请求体类型', - }, - description: { - en_US: 'Type of request body', - zh_Hans: '请求体的类型', - }, - required: false, - default: 'json', - options: [ - { name: 'none', label: { en_US: 'None', zh_Hans: '无' } }, - { name: 'json', label: { en_US: 'JSON', zh_Hans: 'JSON' } }, - { name: 'form', label: { en_US: 'Form Data', zh_Hans: '表单数据' } }, - { name: 'raw', label: { en_US: 'Raw', zh_Hans: '原始' } }, - ], - show_if: { - field: 'method', - operator: 'in', - value: ['POST', 'PUT', 'PATCH'], - }, - }, - { - id: 'body_template', - name: 'body_template', - type: DynamicFormItemType.TEXT, - label: { - en_US: 'Body Template', - zh_Hans: '请求体模板', - }, - description: { - en_US: - 'Request body template (supports variables). Leave empty to use input.', - zh_Hans: '请求体模板(支持变量)。留空则使用输入。', - }, - required: false, - default: '', - show_if: { - field: 'body_type', - operator: 'in', - value: ['json', 'form', 'raw'], - }, - }, - { - 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, - }, - { - id: 'retry_count', - name: 'retry_count', - type: DynamicFormItemType.INT, - label: { - en_US: 'Retry Count', - zh_Hans: '重试次数', - }, - description: { - en_US: 'Number of retries on failure', - zh_Hans: '失败时的重试次数', - }, - required: false, - default: 0, - }, - { - id: 'ignore_ssl', - name: 'ignore_ssl', - type: DynamicFormItemType.BOOLEAN, - label: { - en_US: 'Ignore SSL Errors', - zh_Hans: '忽略 SSL 错误', - }, - description: { - en_US: 'Ignore SSL certificate verification errors', - zh_Hans: '忽略 SSL 证书验证错误', - }, - required: false, - default: false, - }, - ], - defaultConfig: { - method: 'GET', - url: '', - headers: '{}', - body_type: 'json', - body_template: '', - timeout: 30, - retry_count: 0, - ignore_ssl: false, - }, -}; - -/** - * Bot Invoke Node - * Invokes another bot to handle a task - */ -export const botInvokeConfig: NodeConfigMeta = { - nodeType: 'bot_invoke', - label: { - en_US: 'Invoke Bot', - zh_Hans: '调用机器人', - }, - description: { - en_US: 'Invoke another bot to process the input', - zh_Hans: '调用另一个机器人处理输入', - }, - icon: 'Bot', - category: 'action', - color: '#10b981', - inputs: [ - createInput('message', 'string', { - description: 'Message to send to the bot', - label: { en_US: 'Message', zh_Hans: '消息' }, - }), - createInput('context', 'object', { - description: 'Additional context', - label: { en_US: 'Context', zh_Hans: '上下文' }, - required: false, - }), - ], - outputs: [ - createOutput('response', 'string', { - description: 'Bot response', - label: { en_US: 'Response', zh_Hans: '响应' }, - }), - createOutput('success', 'boolean', { - description: 'Whether invocation was successful', - label: { en_US: 'Success', zh_Hans: '成功' }, - }), - ], - configSchema: [ - { - id: 'bot', - name: 'bot', - type: DynamicFormItemType.BOT_SELECTOR, - label: { - en_US: 'Bot', - zh_Hans: '机器人', - }, - description: { - en_US: 'Select the bot to invoke', - zh_Hans: '选择要调用的机器人', - }, - required: true, - default: '', - }, - { - id: 'wait_for_response', - name: 'wait_for_response', - type: DynamicFormItemType.BOOLEAN, - label: { - en_US: 'Wait for Response', - zh_Hans: '等待响应', - }, - description: { - en_US: 'Wait for the bot to respond before continuing', - zh_Hans: '等待机器人响应后再继续', - }, - required: false, - default: true, - }, - { - id: 'timeout', - name: 'timeout', - type: DynamicFormItemType.INT, - label: { - en_US: 'Timeout (seconds)', - zh_Hans: '超时时间(秒)', - }, - description: { - en_US: 'Maximum time to wait for response', - zh_Hans: '等待响应的最大时间', - }, - required: false, - default: 60, - show_if: { - field: 'wait_for_response', - operator: 'eq', - value: true, - }, - }, - ], - defaultConfig: { - bot: '', - wait_for_response: true, - timeout: 60, - }, -}; - -/** - * Workflow Invoke Node - * Invokes another workflow - */ -export const workflowInvokeConfig: NodeConfigMeta = { - nodeType: 'workflow_invoke', - label: { - en_US: 'Invoke Workflow', - zh_Hans: '调用工作流', - }, - description: { - en_US: 'Invoke another workflow as a sub-workflow', - zh_Hans: '调用另一个工作流作为子工作流', - }, - icon: 'Workflow', - category: 'action', - color: '#10b981', - inputs: [ - createInput('input', 'any', { - description: 'Input data for the workflow', - label: { en_US: 'Input', zh_Hans: '输入' }, - required: false, - }), - ], - outputs: [ - createOutput('output', 'any', { - description: 'Workflow output', - label: { en_US: 'Output', zh_Hans: '输出' }, - }), - createOutput('status', 'string', { - description: 'Workflow execution status', - label: { en_US: 'Status', zh_Hans: '状态' }, - }), - createOutput('execution_id', 'string', { - description: 'Workflow execution ID', - label: { en_US: 'Execution ID', zh_Hans: '执行 ID' }, - }), - ], - configSchema: [ - { - id: 'workflow_id', - name: 'workflow_id', - type: DynamicFormItemType.STRING, - label: { - en_US: 'Workflow ID', - zh_Hans: '工作流 ID', - }, - description: { - en_US: 'ID of the workflow to invoke', - zh_Hans: '要调用的工作流 ID', - }, - required: true, - default: '', - }, - { - id: 'wait_for_completion', - name: 'wait_for_completion', - type: DynamicFormItemType.BOOLEAN, - label: { - en_US: 'Wait for Completion', - zh_Hans: '等待完成', - }, - description: { - en_US: 'Wait for the workflow to complete before continuing', - zh_Hans: '等待工作流完成后再继续', - }, - required: false, - default: true, - }, - { - id: 'timeout', - name: 'timeout', - type: DynamicFormItemType.INT, - label: { - en_US: 'Timeout (seconds)', - zh_Hans: '超时时间(秒)', - }, - description: { - en_US: 'Maximum time to wait for workflow completion', - zh_Hans: '等待工作流完成的最大时间', - }, - required: false, - default: 300, - show_if: { - field: 'wait_for_completion', - operator: 'eq', - value: true, - }, - }, - { - id: 'pass_context', - name: 'pass_context', - type: DynamicFormItemType.BOOLEAN, - label: { - en_US: 'Pass Context', - zh_Hans: '传递上下文', - }, - description: { - en_US: 'Pass the current workflow context to the sub-workflow', - zh_Hans: '将当前工作流上下文传递给子工作流', - }, - required: false, - default: false, - }, - ], - defaultConfig: { - workflow_id: '', - wait_for_completion: true, - timeout: 300, - pass_context: false, - }, -}; - -/** - * Notification Node - * Sends notifications via various channels - */ -export const notificationConfig: NodeConfigMeta = { - nodeType: 'notification', - label: { - en_US: 'Notification', - zh_Hans: '通知', - }, - description: { - en_US: 'Send notifications via various channels', - zh_Hans: '通过各种渠道发送通知', - }, - icon: 'Bell', - category: 'action', - color: '#10b981', - inputs: [ - createInput('content', 'string', { - description: 'Notification content', - label: { en_US: 'Content', zh_Hans: '内容' }, - required: false, - }), - ], - outputs: [ - createOutput('success', 'boolean', { - description: 'Whether notification was sent', - label: { en_US: 'Success', zh_Hans: '成功' }, - }), - createOutput('notification_id', 'string', { - description: 'ID of the sent notification', - label: { en_US: 'Notification ID', zh_Hans: '通知 ID' }, - }), - ], - configSchema: [ - { - id: 'channel', - name: 'channel', - type: DynamicFormItemType.SELECT, - label: { - en_US: 'Channel', - zh_Hans: '渠道', - }, - description: { - en_US: 'Notification channel', - zh_Hans: '通知渠道', - }, - required: true, - default: 'webhook', - options: [ - { name: 'webhook', label: { en_US: 'Webhook', zh_Hans: 'Webhook' } }, - { name: 'email', label: { en_US: 'Email', zh_Hans: '邮件' } }, - { name: 'dingtalk', label: { en_US: 'DingTalk', zh_Hans: '钉钉' } }, - { name: 'feishu', label: { en_US: 'Feishu', zh_Hans: '飞书' } }, - { - name: 'wechat_work', - label: { en_US: 'WeChat Work', zh_Hans: '企业微信' }, - }, - ], - }, - { - id: 'title', - name: 'title', - type: DynamicFormItemType.STRING, - label: { - en_US: 'Title', - zh_Hans: '标题', - }, - description: { - en_US: 'Notification title', - zh_Hans: '通知标题', - }, - required: false, - default: '', - }, - { - id: 'content_template', - name: 'content_template', - type: DynamicFormItemType.TEXT, - label: { - en_US: 'Content Template', - zh_Hans: '内容模板', - }, - description: { - en_US: 'Notification content (supports variables)', - zh_Hans: '通知内容(支持变量)', - }, - required: false, - default: '', - }, - { - id: 'webhook_url', - name: 'webhook_url', - type: DynamicFormItemType.STRING, - label: { - en_US: 'Webhook URL', - zh_Hans: 'Webhook URL', - }, - description: { - en_US: 'URL for webhook notifications', - zh_Hans: 'Webhook 通知的 URL', - }, - required: true, - default: '', - show_if: { - field: 'channel', - operator: 'in', - value: ['webhook', 'dingtalk', 'feishu', 'wechat_work'], - }, - }, - { - id: 'recipients', - name: 'recipients', - type: DynamicFormItemType.STRING_ARRAY, - label: { - en_US: 'Recipients', - zh_Hans: '接收者', - }, - description: { - en_US: 'Email recipients or user IDs', - zh_Hans: '邮件接收者或用户 ID', - }, - required: true, - default: [], - show_if: { - field: 'channel', - operator: 'eq', - value: 'email', - }, - }, - ], - defaultConfig: { - channel: 'webhook', - title: '', - content_template: '', - webhook_url: '', - recipients: [], - }, -}; - -/** - * Reply Message Node - * Replies to the message that triggered the workflow - */ -export const replyMessageConfig: NodeConfigMeta = { - nodeType: 'reply_message', - label: { - en_US: 'Reply Message', - zh_Hans: '回复消息', - }, - description: { - en_US: 'Reply to the message that triggered the workflow', - zh_Hans: '回复触发工作流的消息', - }, - icon: 'MessageCircle', - category: 'action', - color: '#10b981', - inputs: [ - createInput('content', 'string', { - description: 'Reply content', - label: { en_US: 'Content', zh_Hans: '内容' }, - required: false, - }), - createInput('context', 'object', { - description: 'Message context (from trigger)', - label: { en_US: 'Context', zh_Hans: '上下文' }, - required: false, - }), - ], - outputs: [ - createOutput('message_id', 'string', { - description: 'ID of the sent reply', - label: { en_US: 'Message ID', zh_Hans: '消息 ID' }, - }), - createOutput('success', 'boolean', { - description: 'Whether the reply was sent successfully', - label: { en_US: 'Success', zh_Hans: '成功' }, - }), - ], - configSchema: [ - { - id: 'reply_mode', - name: 'reply_mode', - type: DynamicFormItemType.SELECT, - label: { en_US: 'Reply Mode', zh_Hans: '回复模式' }, - description: { - en_US: 'How to reply to the original message', - zh_Hans: '如何回复原始消息', - }, - required: true, - default: 'reply', - options: [ - { name: 'reply', label: { en_US: 'Quote Reply', zh_Hans: '引用回复' } }, - { - name: 'direct', - label: { en_US: 'Direct Message', zh_Hans: '直接消息' }, - }, - ], - }, - { - id: 'message_template', - name: 'message_template', - type: DynamicFormItemType.TEXT, - label: { en_US: 'Message Template', zh_Hans: '消息模板' }, - description: { - en_US: - 'Reply content template (supports {{variable}} interpolation). Leave empty to use input.', - zh_Hans: '回复内容模板(支持 {{variable}} 插值)。留空则使用输入。', - }, - required: false, - default: '', - }, - { - id: 'long_text_processing', - name: 'long_text_processing', - type: DynamicFormItemType.SELECT, - label: { en_US: 'Long Text Processing', zh_Hans: '长文本处理' }, - description: { - en_US: 'How to handle long text that exceeds platform limits', - zh_Hans: '如何处理超出平台限制的长文本', - }, - required: false, - default: 'truncate', - options: [ - { name: 'truncate', label: { en_US: 'Truncate', zh_Hans: '截断' } }, - { - name: 'split', - label: { - en_US: 'Split into multiple messages', - zh_Hans: '拆分为多条消息', - }, - }, - { - name: 'forward', - label: { en_US: 'Forward as file', zh_Hans: '转发为文件' }, - }, - ], - }, - ], - defaultConfig: { - reply_mode: 'reply', - message_template: '', - long_text_processing: 'truncate', - }, -}; - -/** - * Store Data Node - * Stores data to persistent storage - */ -export const storeDataConfig: NodeConfigMeta = { - nodeType: 'store_data', - label: { - en_US: 'Store Data', - zh_Hans: '存储数据', - }, - description: { - en_US: 'Store data to persistent storage', - zh_Hans: '将数据存储到持久化存储', - }, - icon: 'Database', - category: 'action', - color: '#10b981', - inputs: [ - createInput('value', 'any', { - description: 'Value to store', - label: { en_US: 'Value', zh_Hans: '值' }, - required: false, - }), - ], - outputs: [ - createOutput('success', 'boolean', { - description: 'Whether storage was successful', - label: { en_US: 'Success', zh_Hans: '成功' }, - }), - ], - configSchema: [ - { - id: 'storage_type', - name: 'storage_type', - type: DynamicFormItemType.SELECT, - label: { en_US: 'Storage Type', zh_Hans: '存储类型' }, - description: { - en_US: 'Type of storage to use', - zh_Hans: '要使用的存储类型', - }, - required: true, - default: 'variable', - options: [ - { - name: 'variable', - label: { en_US: 'Workflow Variable', zh_Hans: '工作流变量' }, - }, - { - name: 'session', - label: { en_US: 'Session Storage', zh_Hans: '会话存储' }, - }, - { - name: 'persistent', - label: { en_US: 'Persistent Storage', zh_Hans: '持久化存储' }, - }, - ], - }, - { - id: 'key', - name: 'key', - type: DynamicFormItemType.STRING, - label: { en_US: 'Key', zh_Hans: '键' }, - description: { - en_US: 'Storage key (supports variable interpolation)', - zh_Hans: '存储键(支持变量插值)', - }, - required: true, - default: '', - }, - { - 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, - }, - ], - defaultConfig: { storage_type: 'variable', key: '', ttl: 0 }, -}; - -/** - * Call Pipeline Node - * Invokes an existing Pipeline - */ -export const callPipelineConfig: NodeConfigMeta = { - nodeType: 'call_pipeline', - label: { - en_US: 'Call Pipeline', - zh_Hans: '调用 Pipeline', - }, - description: { - en_US: 'Invoke an existing Pipeline for processing', - zh_Hans: '调用现有的 Pipeline 进行处理', - }, - icon: 'Workflow', - category: 'action', - color: '#10b981', - inputs: [ - createInput('input', 'any', { - description: 'Input data for the pipeline', - label: { en_US: 'Input', zh_Hans: '输入' }, - required: false, - }), - ], - outputs: [ - createOutput('response', 'string', { - description: 'Pipeline response', - label: { en_US: 'Response', zh_Hans: '响应' }, - }), - createOutput('result', 'object', { - description: 'Pipeline execution result', - label: { en_US: 'Result', zh_Hans: '结果' }, - }), - ], - configSchema: [ - { - id: 'pipeline_uuid', - name: 'pipeline_uuid', - type: DynamicFormItemType.PIPELINE_SELECTOR, - label: { en_US: 'Pipeline', zh_Hans: '流水线' }, - description: { - en_US: 'Select the pipeline to invoke', - zh_Hans: '选择要调用的流水线', - }, - required: true, - default: '', - }, - { - id: 'inherit_context', - name: 'inherit_context', - type: DynamicFormItemType.BOOLEAN, - label: { en_US: 'Inherit Context', zh_Hans: '继承上下文' }, - description: { - en_US: 'Pass the current workflow context to the pipeline', - zh_Hans: '将当前工作流上下文传递给 Pipeline', - }, - required: false, - default: true, - }, - { - 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: 60, - }, - ], - defaultConfig: { pipeline_uuid: '', inherit_context: true, timeout: 60 }, -}; - -/** - * Set Variable Node - * Sets a context variable - */ -export const setVariableConfig: NodeConfigMeta = { - nodeType: 'set_variable', - label: { - en_US: 'Set Variable', - zh_Hans: '设置变量', - }, - description: { - en_US: 'Set a context variable value', - zh_Hans: '设置上下文变量值', - }, - icon: 'Variable', - category: 'action', - color: '#10b981', - inputs: [ - createInput('value', 'any', { - description: 'Value to set', - label: { en_US: 'Value', zh_Hans: '值' }, - required: false, - }), - ], - outputs: [ - createOutput('output', 'any', { - description: 'The set 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 set', - zh_Hans: '要设置的变量名', - }, - required: true, - default: '', - }, - { - id: 'variable_scope', - name: 'variable_scope', - type: DynamicFormItemType.SELECT, - label: { en_US: 'Variable Scope', zh_Hans: '变量作用域' }, - description: { en_US: 'Scope of the variable', zh_Hans: '变量的作用域' }, - required: true, - default: 'workflow', - options: [ - { name: 'workflow', label: { en_US: 'Workflow', zh_Hans: '工作流' } }, - { name: 'session', label: { en_US: 'Session', zh_Hans: '会话' } }, - { name: 'global', label: { en_US: 'Global', zh_Hans: '全局' } }, - ], - }, - { - id: 'value_template', - name: 'value_template', - type: DynamicFormItemType.TEXT, - label: { en_US: 'Value Template', zh_Hans: '值模板' }, - description: { - en_US: - 'Value template (supports {{variable}} interpolation). Leave empty to use input.', - zh_Hans: '值模板(支持 {{variable}} 插值)。留空则使用输入。', - }, - required: false, - default: '', - }, - ], - defaultConfig: { - variable_name: '', - variable_scope: 'workflow', - value_template: '', - }, -}; - -/** - * Opening Statement Node - * Provides conversation opener and suggested questions - */ -export const openingStatementConfig: NodeConfigMeta = { - nodeType: 'opening_statement', - label: { - en_US: 'Opening Statement', - zh_Hans: '对话开场白', - }, - description: { - en_US: 'Provide conversation opener and suggested questions', - zh_Hans: '提供对话开场白和建议问题', - }, - icon: 'MessageSquarePlus', - category: 'action', - color: '#10b981', - inputs: [], - outputs: [ - createOutput('statement', 'string', { - description: 'The opening statement text', - label: { en_US: 'Statement', zh_Hans: '开场白' }, - }), - createOutput('suggestions', 'array', { - description: 'Suggested questions', - label: { en_US: 'Suggestions', zh_Hans: '建议问题' }, - }), - ], - configSchema: [ - { - id: 'statement', - name: 'statement', - type: DynamicFormItemType.TEXT, - label: { en_US: 'Opening Statement', zh_Hans: '开场白' }, - description: { - en_US: 'The opening statement to display', - zh_Hans: '要显示的开场白', - }, - required: true, - default: '', - }, - { - id: 'suggested_questions', - name: 'suggested_questions', - type: DynamicFormItemType.STRING_ARRAY, - label: { en_US: 'Suggested Questions', zh_Hans: '建议问题' }, - description: { - en_US: 'List of suggested questions for the user', - zh_Hans: '给用户的建议问题列表', - }, - required: false, - default: [], - }, - { - id: 'show_suggestions', - name: 'show_suggestions', - type: DynamicFormItemType.BOOLEAN, - label: { en_US: 'Show Suggestions', zh_Hans: '显示建议' }, - description: { - en_US: 'Whether to show suggested questions', - zh_Hans: '是否显示建议问题', - }, - required: false, - default: true, - }, - ], - defaultConfig: { - statement: '', - suggested_questions: [], - show_suggestions: true, - }, -}; - -/** - * All action node configurations - */ -export const actionConfigs: NodeConfigMeta[] = [ - sendMessageConfig, - replyMessageConfig, - httpRequestConfig, - storeDataConfig, - callPipelineConfig, - setVariableConfig, - openingStatementConfig, - botInvokeConfig, - workflowInvokeConfig, - notificationConfig, -]; - -/** - * Get action config by type - */ -export function getActionConfig(nodeType: string): NodeConfigMeta | undefined { - return actionConfigs.find((config) => config.nodeType === nodeType); -} diff --git a/web/src/app/home/workflows/components/workflow-editor/node-configs/ai-configs.ts b/web/src/app/home/workflows/components/workflow-editor/node-configs/ai-configs.ts deleted file mode 100644 index e6ae4742..00000000 --- a/web/src/app/home/workflows/components/workflow-editor/node-configs/ai-configs.ts +++ /dev/null @@ -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); -} diff --git a/web/src/app/home/workflows/components/workflow-editor/node-configs/control-configs.ts b/web/src/app/home/workflows/components/workflow-editor/node-configs/control-configs.ts deleted file mode 100644 index b4a06fe4..00000000 --- a/web/src/app/home/workflows/components/workflow-editor/node-configs/control-configs.ts +++ /dev/null @@ -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); -} diff --git a/web/src/app/home/workflows/components/workflow-editor/node-configs/index.ts b/web/src/app/home/workflows/components/workflow-editor/node-configs/index.ts deleted file mode 100644 index 0bc9071c..00000000 --- a/web/src/app/home/workflows/components/workflow-editor/node-configs/index.ts +++ /dev/null @@ -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 { - const config = getNodeConfig(nodeType); - if (!config) return {}; - - // Build default config from schema defaults - const defaults: Record = {}; - 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, -): { 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); -} diff --git a/web/src/app/home/workflows/components/workflow-editor/node-configs/integration-configs.ts b/web/src/app/home/workflows/components/workflow-editor/node-configs/integration-configs.ts deleted file mode 100644 index d516574f..00000000 --- a/web/src/app/home/workflows/components/workflow-editor/node-configs/integration-configs.ts +++ /dev/null @@ -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); -} diff --git a/web/src/app/home/workflows/components/workflow-editor/node-configs/process-configs.ts b/web/src/app/home/workflows/components/workflow-editor/node-configs/process-configs.ts deleted file mode 100644 index 4382552e..00000000 --- a/web/src/app/home/workflows/components/workflow-editor/node-configs/process-configs.ts +++ /dev/null @@ -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); -} diff --git a/web/src/app/home/workflows/components/workflow-editor/node-configs/trigger-configs.ts b/web/src/app/home/workflows/components/workflow-editor/node-configs/trigger-configs.ts deleted file mode 100644 index 9e96a04e..00000000 --- a/web/src/app/home/workflows/components/workflow-editor/node-configs/trigger-configs.ts +++ /dev/null @@ -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); -} diff --git a/web/src/app/home/workflows/components/workflow-editor/node-configs/types.ts b/web/src/app/home/workflows/components/workflow-editor/node-configs/types.ts deleted file mode 100644 index 516b5be4..00000000 --- a/web/src/app/home/workflows/components/workflow-editor/node-configs/types.ts +++ /dev/null @@ -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; - - /** 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; - -/** - * 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 }); -} diff --git a/web/src/app/home/workflows/components/workflow-editor/workflow-constants.ts b/web/src/app/home/workflows/components/workflow-editor/workflow-constants.ts index 94f0f422..6fc19278 100644 --- a/web/src/app/home/workflows/components/workflow-editor/workflow-constants.ts +++ b/web/src/app/home/workflows/components/workflow-editor/workflow-constants.ts @@ -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 = { + 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 ; - * ``` */ 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; } diff --git a/web/src/app/home/workflows/components/workflow-editor/workflow-i18n.ts b/web/src/app/home/workflows/components/workflow-editor/workflow-i18n.ts index 9ed145a4..7acf75da 100644 --- a/web/src/app/home/workflows/components/workflow-editor/workflow-i18n.ts +++ b/web/src/app/home/workflows/components/workflow-editor/workflow-i18n.ts @@ -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` (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"`). diff --git a/web/src/app/home/workflows/components/workflow-editor/workflow-node-metadata.ts b/web/src/app/home/workflows/components/workflow-editor/workflow-node-metadata.ts index 82dcaf6a..79a645ab 100644 --- a/web/src/app/home/workflows/components/workflow-editor/workflow-node-metadata.ts +++ b/web/src/app/home/workflows/components/workflow-editor/workflow-node-metadata.ts @@ -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 { - 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 | 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([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, diff --git a/web/src/app/home/workflows/store/useWorkflowStore.ts b/web/src/app/home/workflows/store/useWorkflowStore.ts index 8b4c40bc..ddee429e 100644 --- a/web/src/app/home/workflows/store/useWorkflowStore.ts +++ b/web/src/app/home/workflows/store/useWorkflowStore.ts @@ -25,8 +25,8 @@ export interface WorkflowNode extends Node { label: string; type: string; config: Record; - inputs?: { name: string; label?: string; type?: string }[]; - outputs?: { name: string; label?: string; type?: string }[]; + inputs?: { name: string; label?: string | Record; type?: string }[]; + outputs?: { name: string; label?: string | Record; type?: string }[]; }; } @@ -270,9 +270,14 @@ export const useWorkflowStore = create((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 = ( diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index 6064cae7..c4eb9283 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -557,7 +557,7 @@ export interface WorkflowPosition { export interface WorkflowPortDefinition { name: string; - label?: string; + label?: string | Record; type?: string; }