mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
feat: add File Upload node functionality and related components
- Implemented File Upload node with support for uploading files to the sandbox. - Added necessary UI components including node panel and default configurations. - Enhanced workflow constants and enums to include File Upload. - Updated error handling for file upload operations. - Integrated File Upload into existing workflow structure, ensuring compatibility with variable handling and output management. - Added translations for new File Upload features in workflow.json.
This commit is contained in:
@ -505,22 +505,25 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
|||||||
sandbox_provider = SandboxProviderService.get_sandbox_provider(
|
sandbox_provider = SandboxProviderService.get_sandbox_provider(
|
||||||
application_generate_entity.app_config.tenant_id
|
application_generate_entity.app_config.tenant_id
|
||||||
)
|
)
|
||||||
if workflow.version == Workflow.VERSION_DRAFT:
|
try:
|
||||||
sandbox = SandboxService.create_draft(
|
if workflow.version == Workflow.VERSION_DRAFT:
|
||||||
tenant_id=application_generate_entity.app_config.tenant_id,
|
sandbox = SandboxService.create_draft(
|
||||||
app_id=application_generate_entity.app_config.app_id,
|
tenant_id=application_generate_entity.app_config.tenant_id,
|
||||||
user_id=application_generate_entity.user_id,
|
app_id=application_generate_entity.app_config.app_id,
|
||||||
sandbox_provider=sandbox_provider,
|
user_id=application_generate_entity.user_id,
|
||||||
)
|
sandbox_provider=sandbox_provider,
|
||||||
else:
|
)
|
||||||
sandbox = SandboxService.create(
|
else:
|
||||||
tenant_id=application_generate_entity.app_config.tenant_id,
|
sandbox = SandboxService.create(
|
||||||
app_id=application_generate_entity.app_config.app_id,
|
tenant_id=application_generate_entity.app_config.tenant_id,
|
||||||
user_id=application_generate_entity.user_id,
|
app_id=application_generate_entity.app_config.app_id,
|
||||||
sandbox_id=conversation.id,
|
user_id=application_generate_entity.user_id,
|
||||||
sandbox_provider=sandbox_provider,
|
sandbox_id=conversation.id,
|
||||||
)
|
sandbox_provider=sandbox_provider,
|
||||||
graph_layers.append(SandboxLayer(sandbox))
|
)
|
||||||
|
graph_layers.append(SandboxLayer(sandbox))
|
||||||
|
except ValueError as e:
|
||||||
|
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
|
||||||
|
|
||||||
# new thread with request context and contextvars
|
# new thread with request context and contextvars
|
||||||
context = contextvars.copy_context()
|
context = contextvars.copy_context()
|
||||||
|
|||||||
@ -321,22 +321,25 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
|||||||
sandbox_provider = SandboxProviderService.get_sandbox_provider(
|
sandbox_provider = SandboxProviderService.get_sandbox_provider(
|
||||||
application_generate_entity.app_config.tenant_id
|
application_generate_entity.app_config.tenant_id
|
||||||
)
|
)
|
||||||
if workflow.version == Workflow.VERSION_DRAFT:
|
try:
|
||||||
sandbox = SandboxService.create_draft(
|
if workflow.version == Workflow.VERSION_DRAFT:
|
||||||
tenant_id=application_generate_entity.app_config.tenant_id,
|
sandbox = SandboxService.create_draft(
|
||||||
app_id=application_generate_entity.app_config.app_id,
|
tenant_id=application_generate_entity.app_config.tenant_id,
|
||||||
user_id=application_generate_entity.user_id,
|
app_id=application_generate_entity.app_config.app_id,
|
||||||
sandbox_provider=sandbox_provider,
|
user_id=application_generate_entity.user_id,
|
||||||
)
|
sandbox_provider=sandbox_provider,
|
||||||
else:
|
)
|
||||||
sandbox = SandboxService.create(
|
else:
|
||||||
tenant_id=application_generate_entity.app_config.tenant_id,
|
sandbox = SandboxService.create(
|
||||||
app_id=application_generate_entity.app_config.app_id,
|
tenant_id=application_generate_entity.app_config.tenant_id,
|
||||||
user_id=application_generate_entity.user_id,
|
app_id=application_generate_entity.app_config.app_id,
|
||||||
sandbox_id=application_generate_entity.workflow_execution_id,
|
user_id=application_generate_entity.user_id,
|
||||||
sandbox_provider=sandbox_provider,
|
sandbox_id=application_generate_entity.workflow_execution_id,
|
||||||
)
|
sandbox_provider=sandbox_provider,
|
||||||
graph_layers.append(SandboxLayer(sandbox=sandbox))
|
)
|
||||||
|
graph_layers.append(SandboxLayer(sandbox=sandbox))
|
||||||
|
except ValueError as e:
|
||||||
|
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
|
||||||
|
|
||||||
# new thread with request context and contextvars
|
# new thread with request context and contextvars
|
||||||
context = contextvars.copy_context()
|
context = contextvars.copy_context()
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class DifyCliInitializer(AsyncSandboxInitializer):
|
|||||||
|
|
||||||
def initialize(self, sandbox: Sandbox) -> None:
|
def initialize(self, sandbox: Sandbox) -> None:
|
||||||
vm = sandbox.vm
|
vm = sandbox.vm
|
||||||
|
# FIXME(Mairuis): should be more robust, effectively.
|
||||||
binary = self._locator.resolve(vm.metadata.os, vm.metadata.arch)
|
binary = self._locator.resolve(vm.metadata.os, vm.metadata.arch)
|
||||||
|
|
||||||
pipeline(vm).add(
|
pipeline(vm).add(
|
||||||
|
|||||||
@ -64,6 +64,7 @@ class NodeType(StrEnum):
|
|||||||
TRIGGER_PLUGIN = "trigger-plugin"
|
TRIGGER_PLUGIN = "trigger-plugin"
|
||||||
HUMAN_INPUT = "human-input"
|
HUMAN_INPUT = "human-input"
|
||||||
COMMAND = "command"
|
COMMAND = "command"
|
||||||
|
FILE_UPLOAD = "file-upload"
|
||||||
GROUP = "group"
|
GROUP = "group"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@ -17,7 +17,8 @@ from core.workflow.nodes.command.exc import CommandExecutionError
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
COMMAND_NODE_TIMEOUT_SECONDS = 60
|
# FIXME(Mairuis): The timeout value is currently hardcoded and should be made configurable in the future.
|
||||||
|
COMMAND_NODE_TIMEOUT_SECONDS = 60 * 10
|
||||||
|
|
||||||
|
|
||||||
class CommandNode(Node[CommandNodeData]):
|
class CommandNode(Node[CommandNodeData]):
|
||||||
@ -71,8 +72,6 @@ class CommandNode(Node[CommandNodeData]):
|
|||||||
error_type="CommandNodeError",
|
error_type="CommandNodeError",
|
||||||
)
|
)
|
||||||
|
|
||||||
timeout = COMMAND_NODE_TIMEOUT_SECONDS if COMMAND_NODE_TIMEOUT_SECONDS > 0 else None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sandbox.wait_ready(timeout=SANDBOX_READY_TIMEOUT)
|
sandbox.wait_ready(timeout=SANDBOX_READY_TIMEOUT)
|
||||||
with with_connection(sandbox.vm) as conn:
|
with with_connection(sandbox.vm) as conn:
|
||||||
@ -81,7 +80,7 @@ class CommandNode(Node[CommandNodeData]):
|
|||||||
sandbox_debug("command_node", "command", command)
|
sandbox_debug("command_node", "command", command)
|
||||||
|
|
||||||
future = submit_command(sandbox.vm, conn, command, cwd=working_directory)
|
future = submit_command(sandbox.vm, conn, command, cwd=working_directory)
|
||||||
result = future.result(timeout=timeout)
|
result = future.result(timeout=COMMAND_NODE_TIMEOUT_SECONDS)
|
||||||
|
|
||||||
outputs: dict[str, Any] = {
|
outputs: dict[str, Any] = {
|
||||||
"stdout": result.stdout.decode("utf-8", errors="replace"),
|
"stdout": result.stdout.decode("utf-8", errors="replace"),
|
||||||
|
|||||||
4
api/core/workflow/nodes/file_upload/__init__.py
Normal file
4
api/core/workflow/nodes/file_upload/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .entities import FileUploadNodeData
|
||||||
|
from .node import FileUploadNode
|
||||||
|
|
||||||
|
__all__ = ["FileUploadNode", "FileUploadNodeData"]
|
||||||
7
api/core/workflow/nodes/file_upload/entities.py
Normal file
7
api/core/workflow/nodes/file_upload/entities.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from core.workflow.nodes.base import BaseNodeData
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadNodeData(BaseNodeData):
|
||||||
|
variable_selector: Sequence[str]
|
||||||
6
api/core/workflow/nodes/file_upload/exc.py
Normal file
6
api/core/workflow/nodes/file_upload/exc.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
class FileUploadNodeError(ValueError):
|
||||||
|
"""Base exception for errors related to the FileUploadNode."""
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadDownloadError(FileUploadNodeError):
|
||||||
|
"""Exception raised when preparing file download in sandbox fails."""
|
||||||
244
api/core/workflow/nodes/file_upload/node.py
Normal file
244
api/core/workflow/nodes/file_upload/node.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import posixpath
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from pathlib import PurePosixPath
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from core.file import File, FileTransferMethod
|
||||||
|
from core.sandbox.bash.session import SANDBOX_READY_TIMEOUT
|
||||||
|
from core.sandbox.services.asset_download_service import AssetDownloadItem
|
||||||
|
from core.variables import ArrayFileSegment
|
||||||
|
from core.variables.segments import ArrayStringSegment, FileSegment
|
||||||
|
from core.virtual_environment.__base.command_future import CommandCancelledError, CommandTimeoutError
|
||||||
|
from core.virtual_environment.__base.helpers import pipeline
|
||||||
|
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
|
||||||
|
from core.workflow.node_events import NodeRunResult
|
||||||
|
from core.workflow.nodes.base.node import Node
|
||||||
|
|
||||||
|
from .entities import FileUploadNodeData
|
||||||
|
from .exc import FileUploadDownloadError, FileUploadNodeError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadNode(Node[FileUploadNodeData]):
|
||||||
|
"""Upload workflow file variables into sandbox via presigned URLs.
|
||||||
|
|
||||||
|
The node intentionally avoids streaming file bytes through Dify workers. For local/tool
|
||||||
|
files, it generates storage-backed presigned URLs and lets sandbox download directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node_type = NodeType.FILE_UPLOAD
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def version(cls) -> str:
|
||||||
|
return "1"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]:
|
||||||
|
_ = filters
|
||||||
|
return {
|
||||||
|
"type": "file-upload",
|
||||||
|
"config": {
|
||||||
|
"variable_selector": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _run(self) -> NodeRunResult:
|
||||||
|
sandbox = self.graph_runtime_state.sandbox
|
||||||
|
variable_selector = self.node_data.variable_selector
|
||||||
|
inputs: dict[str, Any] = {"variable_selector": variable_selector}
|
||||||
|
if sandbox is None:
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
error="Sandbox not available for FileUploadNode.",
|
||||||
|
error_type="SandboxNotInitializedError",
|
||||||
|
inputs=inputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
variable = self.graph_runtime_state.variable_pool.get(variable_selector)
|
||||||
|
if variable is None:
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
error=f"File variable not found for selector: {variable_selector}",
|
||||||
|
error_type=FileUploadNodeError.__name__,
|
||||||
|
inputs=inputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if variable.value and not isinstance(variable, ArrayFileSegment | FileSegment):
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
error=f"Variable {variable_selector} is not a file or file array",
|
||||||
|
error_type=FileUploadNodeError.__name__,
|
||||||
|
inputs=inputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
files = self._normalize_files(variable.value)
|
||||||
|
process_data: dict[str, Any] = {
|
||||||
|
"file_count": len(files),
|
||||||
|
"files": [file.to_dict() for file in files],
|
||||||
|
}
|
||||||
|
if not files:
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||||
|
error="Selected file variable is empty.",
|
||||||
|
error_type=FileUploadNodeError.__name__,
|
||||||
|
inputs=inputs,
|
||||||
|
process_data=process_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sandbox.wait_ready(timeout=SANDBOX_READY_TIMEOUT)
|
||||||
|
download_items: list[AssetDownloadItem] = self._build_download_items(files)
|
||||||
|
sandbox_paths = self._upload(sandbox.vm, download_items)
|
||||||
|
file_names = [PurePosixPath(path).name for path in sandbox_paths]
|
||||||
|
process_data = {
|
||||||
|
**process_data,
|
||||||
|
"sandbox_paths": sandbox_paths,
|
||||||
|
"file_names": file_names,
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs: dict[str, Any]
|
||||||
|
if len(sandbox_paths) == 1:
|
||||||
|
outputs = {
|
||||||
|
"sandbox_path": sandbox_paths[0],
|
||||||
|
"file_name": file_names[0],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
outputs = {
|
||||||
|
"sandbox_path": ArrayStringSegment(value=sandbox_paths),
|
||||||
|
"file_name": ArrayStringSegment(value=file_names),
|
||||||
|
}
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||||
|
inputs=inputs,
|
||||||
|
process_data=process_data,
|
||||||
|
outputs=outputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
except CommandTimeoutError:
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
error="File upload timeout",
|
||||||
|
error_type=CommandTimeoutError.__name__,
|
||||||
|
inputs=inputs,
|
||||||
|
process_data=process_data,
|
||||||
|
)
|
||||||
|
except CommandCancelledError:
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
error="File upload command was cancelled",
|
||||||
|
error_type=CommandCancelledError.__name__,
|
||||||
|
inputs=inputs,
|
||||||
|
process_data=process_data,
|
||||||
|
)
|
||||||
|
except FileUploadNodeError as e:
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
error=str(e),
|
||||||
|
error_type=type(e).__name__,
|
||||||
|
inputs=inputs,
|
||||||
|
process_data=process_data,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("File upload node %s failed", self.id)
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
error=str(e),
|
||||||
|
error_type=type(e).__name__,
|
||||||
|
inputs=inputs,
|
||||||
|
process_data=process_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_variable_selector_to_variable_mapping(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
graph_config: Mapping[str, Any],
|
||||||
|
node_id: str,
|
||||||
|
node_data: Mapping[str, Any],
|
||||||
|
) -> Mapping[str, Sequence[str]]:
|
||||||
|
_ = graph_config
|
||||||
|
typed_node_data = FileUploadNodeData.model_validate(node_data)
|
||||||
|
return {node_id + ".files": typed_node_data.variable_selector}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_files(value: Any) -> list[File]:
|
||||||
|
if isinstance(value, File):
|
||||||
|
return [value]
|
||||||
|
if isinstance(value, list):
|
||||||
|
list_value = cast(list[object], value)
|
||||||
|
files: list[File] = []
|
||||||
|
for idx in range(len(list_value)):
|
||||||
|
candidate = list_value[idx]
|
||||||
|
if not isinstance(candidate, File):
|
||||||
|
return []
|
||||||
|
files.append(candidate)
|
||||||
|
return files
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _build_download_items(self, files: Sequence[File]) -> list[AssetDownloadItem]:
|
||||||
|
used_paths: set[str] = set()
|
||||||
|
items: list[AssetDownloadItem] = []
|
||||||
|
for index, file in enumerate(files):
|
||||||
|
file_url = self._get_download_url(file)
|
||||||
|
|
||||||
|
filename = (file.filename or "").strip()
|
||||||
|
if not filename or filename in {".", ".."}:
|
||||||
|
filename = f"file-{index + 1}{file.extension or ''}"
|
||||||
|
filename = os.path.basename(filename)
|
||||||
|
|
||||||
|
if filename in used_paths:
|
||||||
|
stem = PurePosixPath(filename).stem or f"file-{index + 1}"
|
||||||
|
suffix = PurePosixPath(filename).suffix
|
||||||
|
dedupe = 1
|
||||||
|
while filename in used_paths:
|
||||||
|
filename = f"{stem}_{dedupe}{suffix}"
|
||||||
|
dedupe += 1
|
||||||
|
|
||||||
|
used_paths.add(filename)
|
||||||
|
items.append(AssetDownloadItem(path=filename, url=file_url))
|
||||||
|
return items
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_path(path: str) -> str:
|
||||||
|
normalized = posixpath.normpath(path.strip()) if path else "."
|
||||||
|
if normalized.startswith("/"):
|
||||||
|
normalized = normalized.lstrip("/")
|
||||||
|
return normalized or "."
|
||||||
|
|
||||||
|
def _upload(self, vm: Any, items: list[AssetDownloadItem]) -> list[str]:
|
||||||
|
p = pipeline(vm)
|
||||||
|
out_paths: list[str] = []
|
||||||
|
for item in items:
|
||||||
|
out_path = self._normalize_path(item.path)
|
||||||
|
if out_path in ("", "."):
|
||||||
|
raise FileUploadDownloadError("Download item path must point to a file")
|
||||||
|
out_paths.append(out_path)
|
||||||
|
p.add(["curl", "-fsSL", item.url, "-o", out_path], error_message="Failed to download file")
|
||||||
|
|
||||||
|
try:
|
||||||
|
p.execute(timeout=None, raise_on_error=True)
|
||||||
|
except Exception as exc:
|
||||||
|
raise FileUploadDownloadError(str(exc)) from exc
|
||||||
|
|
||||||
|
return out_paths
|
||||||
|
|
||||||
|
def _get_download_url(self, file: File) -> str:
|
||||||
|
if file.transfer_method == FileTransferMethod.REMOTE_URL:
|
||||||
|
if not file.remote_url:
|
||||||
|
raise FileUploadDownloadError("Remote file URL is missing")
|
||||||
|
return file.remote_url
|
||||||
|
|
||||||
|
if file.transfer_method in (
|
||||||
|
FileTransferMethod.LOCAL_FILE,
|
||||||
|
FileTransferMethod.TOOL_FILE,
|
||||||
|
FileTransferMethod.DATASOURCE_FILE,
|
||||||
|
):
|
||||||
|
download_url = file.generate_url(for_external=True)
|
||||||
|
if not download_url:
|
||||||
|
raise FileUploadDownloadError("Unable to generate download URL for file")
|
||||||
|
return download_url
|
||||||
|
|
||||||
|
raise FileUploadDownloadError(f"Unsupported file transfer method: {file.transfer_method}")
|
||||||
@ -58,6 +58,7 @@ const DEFAULT_ICON_MAP: Record<BlockEnum, React.ComponentType<{ className: strin
|
|||||||
[BlockEnum.LLM]: Llm,
|
[BlockEnum.LLM]: Llm,
|
||||||
[BlockEnum.Code]: Code,
|
[BlockEnum.Code]: Code,
|
||||||
[BlockEnum.Command]: WindowCursor,
|
[BlockEnum.Command]: WindowCursor,
|
||||||
|
[BlockEnum.FileUpload]: DocsExtractor,
|
||||||
[BlockEnum.End]: End,
|
[BlockEnum.End]: End,
|
||||||
[BlockEnum.IfElse]: IfElse,
|
[BlockEnum.IfElse]: IfElse,
|
||||||
[BlockEnum.HttpRequest]: Http,
|
[BlockEnum.HttpRequest]: Http,
|
||||||
@ -100,6 +101,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
|||||||
[BlockEnum.LLM]: 'bg-util-colors-indigo-indigo-500',
|
[BlockEnum.LLM]: 'bg-util-colors-indigo-indigo-500',
|
||||||
[BlockEnum.Code]: 'bg-util-colors-blue-blue-500',
|
[BlockEnum.Code]: 'bg-util-colors-blue-blue-500',
|
||||||
[BlockEnum.Command]: 'bg-util-colors-blue-blue-500',
|
[BlockEnum.Command]: 'bg-util-colors-blue-blue-500',
|
||||||
|
[BlockEnum.FileUpload]: 'bg-util-colors-green-green-500',
|
||||||
[BlockEnum.End]: 'bg-util-colors-warning-warning-500',
|
[BlockEnum.End]: 'bg-util-colors-warning-warning-500',
|
||||||
[BlockEnum.IfElse]: 'bg-util-colors-cyan-cyan-500',
|
[BlockEnum.IfElse]: 'bg-util-colors-cyan-cyan-500',
|
||||||
[BlockEnum.Iteration]: 'bg-util-colors-cyan-cyan-500',
|
[BlockEnum.Iteration]: 'bg-util-colors-cyan-cyan-500',
|
||||||
|
|||||||
@ -152,6 +152,11 @@ export const BLOCKS = [
|
|||||||
type: BlockEnum.Command,
|
type: BlockEnum.Command,
|
||||||
title: 'Command',
|
title: 'Command',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
classification: BlockClassificationEnum.Utilities,
|
||||||
|
type: BlockEnum.FileUpload,
|
||||||
|
title: 'File Upload',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
classification: BlockClassificationEnum.Default,
|
classification: BlockClassificationEnum.Default,
|
||||||
type: BlockEnum.Agent,
|
type: BlockEnum.Agent,
|
||||||
|
|||||||
@ -118,6 +118,7 @@ export const SUPPORT_OUTPUT_VARS_NODE = [
|
|||||||
BlockEnum.Code,
|
BlockEnum.Code,
|
||||||
BlockEnum.TemplateTransform,
|
BlockEnum.TemplateTransform,
|
||||||
BlockEnum.Command,
|
BlockEnum.Command,
|
||||||
|
BlockEnum.FileUpload,
|
||||||
BlockEnum.HttpRequest,
|
BlockEnum.HttpRequest,
|
||||||
BlockEnum.Tool,
|
BlockEnum.Tool,
|
||||||
BlockEnum.VariableAssigner,
|
BlockEnum.VariableAssigner,
|
||||||
@ -216,6 +217,17 @@ export const COMMAND_OUTPUT_STRUCT: Var[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const FILE_UPLOAD_OUTPUT_STRUCT: Var[] = [
|
||||||
|
{
|
||||||
|
variable: 'sandbox_path',
|
||||||
|
type: VarType.string,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variable: 'file_name',
|
||||||
|
type: VarType.string,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export const QUESTION_CLASSIFIER_OUTPUT_STRUCT = [
|
export const QUESTION_CLASSIFIER_OUTPUT_STRUCT = [
|
||||||
{
|
{
|
||||||
variable: 'class_name',
|
variable: 'class_name',
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import agentDefault from '@/app/components/workflow/nodes/agent/default'
|
|||||||
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
|
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
|
||||||
import codeDefault from '@/app/components/workflow/nodes/code/default'
|
import codeDefault from '@/app/components/workflow/nodes/code/default'
|
||||||
import commandDefault from '@/app/components/workflow/nodes/command/default'
|
import commandDefault from '@/app/components/workflow/nodes/command/default'
|
||||||
|
|
||||||
import documentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
import documentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
||||||
|
|
||||||
|
import fileUploadDefault from '@/app/components/workflow/nodes/file-upload/default'
|
||||||
|
|
||||||
import httpRequestDefault from '@/app/components/workflow/nodes/http/default'
|
import httpRequestDefault from '@/app/components/workflow/nodes/http/default'
|
||||||
import humanInputDefault from '@/app/components/workflow/nodes/human-input/default'
|
import humanInputDefault from '@/app/components/workflow/nodes/human-input/default'
|
||||||
import ifElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
import ifElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
||||||
@ -36,6 +37,7 @@ export const WORKFLOW_COMMON_NODES = [
|
|||||||
loopEndDefault,
|
loopEndDefault,
|
||||||
codeDefault,
|
codeDefault,
|
||||||
commandDefault,
|
commandDefault,
|
||||||
|
fileUploadDefault,
|
||||||
templateTransformDefault,
|
templateTransformDefault,
|
||||||
variableAggregatorDefault,
|
variableAggregatorDefault,
|
||||||
documentExtractorDefault,
|
documentExtractorDefault,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { CodeNodeType } from '../../../code/types'
|
|||||||
import type { CommandNodeType } from '../../../command/types'
|
import type { CommandNodeType } from '../../../command/types'
|
||||||
import type { DocExtractorNodeType } from '../../../document-extractor/types'
|
import type { DocExtractorNodeType } from '../../../document-extractor/types'
|
||||||
import type { EndNodeType } from '../../../end/types'
|
import type { EndNodeType } from '../../../end/types'
|
||||||
|
import type { FileUploadNodeType } from '../../../file-upload/types'
|
||||||
import type { HttpNodeType } from '../../../http/types'
|
import type { HttpNodeType } from '../../../http/types'
|
||||||
import type { IfElseNodeType } from '../../../if-else/types'
|
import type { IfElseNodeType } from '../../../if-else/types'
|
||||||
import type { IterationNodeType } from '../../../iteration/types'
|
import type { IterationNodeType } from '../../../iteration/types'
|
||||||
@ -42,6 +43,7 @@ import {
|
|||||||
AGENT_OUTPUT_STRUCT,
|
AGENT_OUTPUT_STRUCT,
|
||||||
COMMAND_OUTPUT_STRUCT,
|
COMMAND_OUTPUT_STRUCT,
|
||||||
FILE_STRUCT,
|
FILE_STRUCT,
|
||||||
|
FILE_UPLOAD_OUTPUT_STRUCT,
|
||||||
getGlobalVars,
|
getGlobalVars,
|
||||||
HTTP_REQUEST_OUTPUT_STRUCT,
|
HTTP_REQUEST_OUTPUT_STRUCT,
|
||||||
HUMAN_INPUT_OUTPUT_STRUCT,
|
HUMAN_INPUT_OUTPUT_STRUCT,
|
||||||
@ -471,6 +473,22 @@ const formatItem = (
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case BlockEnum.FileUpload: {
|
||||||
|
res.vars = (data as FileUploadNodeType).is_array_file
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
variable: 'sandbox_path',
|
||||||
|
type: VarType.arrayString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variable: 'file_name',
|
||||||
|
type: VarType.arrayString,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: FILE_UPLOAD_OUTPUT_STRUCT
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case BlockEnum.QuestionClassifier: {
|
case BlockEnum.QuestionClassifier: {
|
||||||
res.vars = QUESTION_CLASSIFIER_OUTPUT_STRUCT
|
res.vars = QUESTION_CLASSIFIER_OUTPUT_STRUCT
|
||||||
break
|
break
|
||||||
@ -1538,6 +1556,11 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
|||||||
])
|
])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case BlockEnum.FileUpload: {
|
||||||
|
const payload = data as FileUploadNodeType
|
||||||
|
res = [payload.variable_selector]
|
||||||
|
break
|
||||||
|
}
|
||||||
case BlockEnum.QuestionClassifier: {
|
case BlockEnum.QuestionClassifier: {
|
||||||
const payload = data as QuestionClassifierNodeType
|
const payload = data as QuestionClassifierNodeType
|
||||||
res = [payload.query_variable_selector]
|
res = [payload.query_variable_selector]
|
||||||
@ -1933,6 +1956,12 @@ export const updateNodeVars = (
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case BlockEnum.FileUpload: {
|
||||||
|
const payload = data as FileUploadNodeType
|
||||||
|
if (payload.variable_selector.join('.') === oldVarSelector.join('.'))
|
||||||
|
payload.variable_selector = newVarSelector
|
||||||
|
break
|
||||||
|
}
|
||||||
case BlockEnum.QuestionClassifier: {
|
case BlockEnum.QuestionClassifier: {
|
||||||
const payload = data as QuestionClassifierNodeType
|
const payload = data as QuestionClassifierNodeType
|
||||||
if (
|
if (
|
||||||
@ -2232,6 +2261,17 @@ export const getNodeOutputVars = (
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case BlockEnum.FileUpload: {
|
||||||
|
if ((data as FileUploadNodeType).is_array_file) {
|
||||||
|
res.push([id, 'sandbox_path'])
|
||||||
|
res.push([id, 'file_name'])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
varsToValueSelectorList(FILE_UPLOAD_OUTPUT_STRUCT, [id], res)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case BlockEnum.QuestionClassifier: {
|
case BlockEnum.QuestionClassifier: {
|
||||||
varsToValueSelectorList(QUESTION_CLASSIFIER_OUTPUT_STRUCT, [id], res)
|
varsToValueSelectorList(QUESTION_CLASSIFIER_OUTPUT_STRUCT, [id], res)
|
||||||
break
|
break
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import useAgentSingleRunFormParams from '@/app/components/workflow/nodes/agent/u
|
|||||||
import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params'
|
import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params'
|
||||||
import useCodeSingleRunFormParams from '@/app/components/workflow/nodes/code/use-single-run-form-params'
|
import useCodeSingleRunFormParams from '@/app/components/workflow/nodes/code/use-single-run-form-params'
|
||||||
import useDocExtractorSingleRunFormParams from '@/app/components/workflow/nodes/document-extractor/use-single-run-form-params'
|
import useDocExtractorSingleRunFormParams from '@/app/components/workflow/nodes/document-extractor/use-single-run-form-params'
|
||||||
|
import useFileUploadSingleRunFormParams from '@/app/components/workflow/nodes/file-upload/use-single-run-form-params'
|
||||||
import useHttpRequestSingleRunFormParams from '@/app/components/workflow/nodes/http/use-single-run-form-params'
|
import useHttpRequestSingleRunFormParams from '@/app/components/workflow/nodes/http/use-single-run-form-params'
|
||||||
import useHumanInputSingleRunFormParams from '@/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params'
|
import useHumanInputSingleRunFormParams from '@/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params'
|
||||||
import useIfElseSingleRunFormParams from '@/app/components/workflow/nodes/if-else/use-single-run-form-params'
|
import useIfElseSingleRunFormParams from '@/app/components/workflow/nodes/if-else/use-single-run-form-params'
|
||||||
@ -51,6 +52,7 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
|||||||
[BlockEnum.KnowledgeRetrieval]: useKnowledgeRetrievalSingleRunFormParams,
|
[BlockEnum.KnowledgeRetrieval]: useKnowledgeRetrievalSingleRunFormParams,
|
||||||
[BlockEnum.Code]: useCodeSingleRunFormParams,
|
[BlockEnum.Code]: useCodeSingleRunFormParams,
|
||||||
[BlockEnum.Command]: undefined,
|
[BlockEnum.Command]: undefined,
|
||||||
|
[BlockEnum.FileUpload]: useFileUploadSingleRunFormParams,
|
||||||
[BlockEnum.TemplateTransform]: useTemplateTransformSingleRunFormParams,
|
[BlockEnum.TemplateTransform]: useTemplateTransformSingleRunFormParams,
|
||||||
[BlockEnum.QuestionClassifier]: useQuestionClassifierSingleRunFormParams,
|
[BlockEnum.QuestionClassifier]: useQuestionClassifierSingleRunFormParams,
|
||||||
[BlockEnum.HttpRequest]: useHttpRequestSingleRunFormParams,
|
[BlockEnum.HttpRequest]: useHttpRequestSingleRunFormParams,
|
||||||
@ -93,6 +95,7 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
|
|||||||
[BlockEnum.KnowledgeRetrieval]: undefined,
|
[BlockEnum.KnowledgeRetrieval]: undefined,
|
||||||
[BlockEnum.Code]: undefined,
|
[BlockEnum.Code]: undefined,
|
||||||
[BlockEnum.Command]: undefined,
|
[BlockEnum.Command]: undefined,
|
||||||
|
[BlockEnum.FileUpload]: undefined,
|
||||||
[BlockEnum.TemplateTransform]: undefined,
|
[BlockEnum.TemplateTransform]: undefined,
|
||||||
[BlockEnum.QuestionClassifier]: undefined,
|
[BlockEnum.QuestionClassifier]: undefined,
|
||||||
[BlockEnum.HttpRequest]: undefined,
|
[BlockEnum.HttpRequest]: undefined,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { getNodeInfoById, isConversationVar, isENV, isSystemVar, toNodeOutputVar
|
|||||||
import Assigner from '@/app/components/workflow/nodes/assigner/default'
|
import Assigner from '@/app/components/workflow/nodes/assigner/default'
|
||||||
import CodeDefault from '@/app/components/workflow/nodes/code/default'
|
import CodeDefault from '@/app/components/workflow/nodes/code/default'
|
||||||
import DocumentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
import DocumentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
||||||
|
import FileUploadDefault from '@/app/components/workflow/nodes/file-upload/default'
|
||||||
import HTTPDefault from '@/app/components/workflow/nodes/http/default'
|
import HTTPDefault from '@/app/components/workflow/nodes/http/default'
|
||||||
import HumanInputDefault from '@/app/components/workflow/nodes/human-input/default'
|
import HumanInputDefault from '@/app/components/workflow/nodes/human-input/default'
|
||||||
import IfElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
import IfElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
||||||
@ -70,6 +71,7 @@ const { checkValid: checkAssignerValid } = Assigner
|
|||||||
const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault
|
const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault
|
||||||
const { checkValid: checkIterationValid } = IterationDefault
|
const { checkValid: checkIterationValid } = IterationDefault
|
||||||
const { checkValid: checkDocumentExtractorValid } = DocumentExtractorDefault
|
const { checkValid: checkDocumentExtractorValid } = DocumentExtractorDefault
|
||||||
|
const { checkValid: checkFileUploadValid } = FileUploadDefault
|
||||||
const { checkValid: checkLoopValid } = LoopDefault
|
const { checkValid: checkLoopValid } = LoopDefault
|
||||||
const { checkValid: checkHumanInputValid } = HumanInputDefault
|
const { checkValid: checkHumanInputValid } = HumanInputDefault
|
||||||
|
|
||||||
@ -88,6 +90,7 @@ const checkValidFns: Partial<Record<BlockEnum, Function>> = {
|
|||||||
[BlockEnum.ParameterExtractor]: checkParameterExtractorValid,
|
[BlockEnum.ParameterExtractor]: checkParameterExtractorValid,
|
||||||
[BlockEnum.Iteration]: checkIterationValid,
|
[BlockEnum.Iteration]: checkIterationValid,
|
||||||
[BlockEnum.DocExtractor]: checkDocumentExtractorValid,
|
[BlockEnum.DocExtractor]: checkDocumentExtractorValid,
|
||||||
|
[BlockEnum.FileUpload]: checkFileUploadValid,
|
||||||
[BlockEnum.Loop]: checkLoopValid,
|
[BlockEnum.Loop]: checkLoopValid,
|
||||||
[BlockEnum.HumanInput]: checkHumanInputValid,
|
[BlockEnum.HumanInput]: checkHumanInputValid,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import DocExtractorNode from './document-extractor/node'
|
|||||||
import DocExtractorPanel from './document-extractor/panel'
|
import DocExtractorPanel from './document-extractor/panel'
|
||||||
import EndNode from './end/node'
|
import EndNode from './end/node'
|
||||||
import EndPanel from './end/panel'
|
import EndPanel from './end/panel'
|
||||||
|
import FileUploadNode from './file-upload/node'
|
||||||
|
import FileUploadPanel from './file-upload/panel'
|
||||||
import GroupNode from './group/node'
|
import GroupNode from './group/node'
|
||||||
import GroupPanel from './group/panel'
|
import GroupPanel from './group/panel'
|
||||||
import HttpNode from './http/node'
|
import HttpNode from './http/node'
|
||||||
@ -83,6 +85,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
|||||||
[BlockEnum.TriggerWebhook]: TriggerWebhookNode,
|
[BlockEnum.TriggerWebhook]: TriggerWebhookNode,
|
||||||
[BlockEnum.TriggerPlugin]: TriggerPluginNode,
|
[BlockEnum.TriggerPlugin]: TriggerPluginNode,
|
||||||
[BlockEnum.Command]: CommandNode,
|
[BlockEnum.Command]: CommandNode,
|
||||||
|
[BlockEnum.FileUpload]: FileUploadNode,
|
||||||
[BlockEnum.Group]: GroupNode,
|
[BlockEnum.Group]: GroupNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,5 +117,6 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
|||||||
[BlockEnum.TriggerWebhook]: TriggerWebhookPanel,
|
[BlockEnum.TriggerWebhook]: TriggerWebhookPanel,
|
||||||
[BlockEnum.TriggerPlugin]: TriggerPluginPanel,
|
[BlockEnum.TriggerPlugin]: TriggerPluginPanel,
|
||||||
[BlockEnum.Command]: CommandPanel,
|
[BlockEnum.Command]: CommandPanel,
|
||||||
|
[BlockEnum.FileUpload]: FileUploadPanel,
|
||||||
[BlockEnum.Group]: GroupPanel,
|
[BlockEnum.Group]: GroupPanel,
|
||||||
}
|
}
|
||||||
|
|||||||
35
web/app/components/workflow/nodes/file-upload/default.ts
Normal file
35
web/app/components/workflow/nodes/file-upload/default.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import type { NodeDefault } from '../../types'
|
||||||
|
import type { FileUploadNodeType } from './types'
|
||||||
|
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||||
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
|
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||||
|
|
||||||
|
const i18nPrefix = 'errorMsg'
|
||||||
|
|
||||||
|
const metaData = genNodeMetaData({
|
||||||
|
classification: BlockClassificationEnum.Utilities,
|
||||||
|
sort: 3,
|
||||||
|
type: BlockEnum.FileUpload,
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeDefault: NodeDefault<FileUploadNodeType> = {
|
||||||
|
metaData,
|
||||||
|
defaultValue: {
|
||||||
|
variable_selector: [],
|
||||||
|
is_array_file: false,
|
||||||
|
},
|
||||||
|
checkValid(payload: FileUploadNodeType, t: (key: string, options?: Record<string, unknown>) => string) {
|
||||||
|
let errorMessages = ''
|
||||||
|
const { variable_selector: variable } = payload
|
||||||
|
|
||||||
|
if (!errorMessages && !variable?.length)
|
||||||
|
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.fileVariable`, { ns: 'workflow' }) })
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: !errorMessages,
|
||||||
|
errorMessage: errorMessages,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nodeDefault
|
||||||
12
web/app/components/workflow/nodes/file-upload/node.tsx
Normal file
12
web/app/components/workflow/nodes/file-upload/node.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import type { FileUploadNodeType } from './types'
|
||||||
|
import type { NodeProps } from '@/app/components/workflow/types'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
const Node: FC<NodeProps<FileUploadNodeType>> = () => {
|
||||||
|
return (
|
||||||
|
<div></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Node)
|
||||||
65
web/app/components/workflow/nodes/file-upload/panel.tsx
Normal file
65
web/app/components/workflow/nodes/file-upload/panel.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import type { FileUploadNodeType } from './types'
|
||||||
|
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||||
|
import * as React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||||
|
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||||
|
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||||
|
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||||
|
import useConfig from './use-config'
|
||||||
|
|
||||||
|
const i18nPrefix = 'nodes.fileUpload'
|
||||||
|
|
||||||
|
const Panel: FC<NodePanelProps<FileUploadNodeType>> = ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const {
|
||||||
|
readOnly,
|
||||||
|
inputs,
|
||||||
|
handleVarChanges,
|
||||||
|
filterVar,
|
||||||
|
} = useConfig(id, data)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="space-y-4 px-4 pb-4">
|
||||||
|
<Field
|
||||||
|
title={t(`${i18nPrefix}.inputVar`, { ns: 'workflow' })}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<VarReferencePicker
|
||||||
|
readonly={readOnly}
|
||||||
|
nodeId={id}
|
||||||
|
isShowNodeName
|
||||||
|
value={inputs.variable_selector || []}
|
||||||
|
onChange={handleVarChanges}
|
||||||
|
filterVar={filterVar}
|
||||||
|
typePlaceHolder="File | Array[File]"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<Split />
|
||||||
|
<div>
|
||||||
|
<OutputVars>
|
||||||
|
<>
|
||||||
|
<VarItem
|
||||||
|
name="sandbox_path"
|
||||||
|
type={inputs.is_array_file ? 'array[string]' : 'string'}
|
||||||
|
description={t(`${i18nPrefix}.outputVars.sandboxPath`, { ns: 'workflow' })}
|
||||||
|
/>
|
||||||
|
<VarItem
|
||||||
|
name="file_name"
|
||||||
|
type={inputs.is_array_file ? 'array[string]' : 'string'}
|
||||||
|
description={t(`${i18nPrefix}.outputVars.fileName`, { ns: 'workflow' })}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</OutputVars>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Panel)
|
||||||
6
web/app/components/workflow/nodes/file-upload/types.ts
Normal file
6
web/app/components/workflow/nodes/file-upload/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
export type FileUploadNodeType = CommonNodeType & {
|
||||||
|
variable_selector: ValueSelector
|
||||||
|
is_array_file: boolean
|
||||||
|
}
|
||||||
65
web/app/components/workflow/nodes/file-upload/use-config.ts
Normal file
65
web/app/components/workflow/nodes/file-upload/use-config.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { ValueSelector, Var } from '../../types'
|
||||||
|
import type { FileUploadNodeType } from './types'
|
||||||
|
import { produce } from 'immer'
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
import {
|
||||||
|
useIsChatMode,
|
||||||
|
useNodesReadOnly,
|
||||||
|
useWorkflow,
|
||||||
|
useWorkflowVariables,
|
||||||
|
} from '@/app/components/workflow/hooks'
|
||||||
|
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||||
|
import { VarType } from '../../types'
|
||||||
|
|
||||||
|
const useConfig = (id: string, payload: FileUploadNodeType) => {
|
||||||
|
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||||
|
const { inputs, setInputs } = useNodeCrud<FileUploadNodeType>(id, payload)
|
||||||
|
|
||||||
|
const filterVar = useCallback((varPayload: Var) => {
|
||||||
|
return varPayload.type === VarType.file || varPayload.type === VarType.arrayFile
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isChatMode = useIsChatMode()
|
||||||
|
const store = useStoreApi()
|
||||||
|
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||||
|
const { getNodes } = store.getState()
|
||||||
|
const currentNode = getNodes().find(n => n.id === id)
|
||||||
|
const isInIteration = payload.isInIteration
|
||||||
|
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||||
|
const isInLoop = payload.isInLoop
|
||||||
|
const loopNode = isInLoop ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||||
|
|
||||||
|
const availableNodes = useMemo(() => {
|
||||||
|
return getBeforeNodesInSameBranch(id)
|
||||||
|
}, [getBeforeNodesInSameBranch, id])
|
||||||
|
|
||||||
|
const { getCurrentVariableType } = useWorkflowVariables()
|
||||||
|
const getType = useCallback((variable?: ValueSelector) => {
|
||||||
|
const varType = getCurrentVariableType({
|
||||||
|
parentNode: isInIteration ? iterationNode : loopNode,
|
||||||
|
valueSelector: variable || [],
|
||||||
|
availableNodes,
|
||||||
|
isChatMode,
|
||||||
|
isConstant: false,
|
||||||
|
})
|
||||||
|
return varType
|
||||||
|
}, [getCurrentVariableType, isInIteration, iterationNode, loopNode, availableNodes, isChatMode])
|
||||||
|
|
||||||
|
const handleVarChanges = useCallback((variable: ValueSelector | string) => {
|
||||||
|
const newInputs = produce(inputs, (draft) => {
|
||||||
|
draft.variable_selector = variable as ValueSelector
|
||||||
|
draft.is_array_file = getType(draft.variable_selector) === VarType.arrayFile
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}, [inputs, setInputs, getType])
|
||||||
|
|
||||||
|
return {
|
||||||
|
readOnly,
|
||||||
|
inputs,
|
||||||
|
filterVar,
|
||||||
|
handleVarChanges,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useConfig
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import type { RefObject } from 'react'
|
||||||
|
import type { FileUploadNodeType } from './types'
|
||||||
|
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { InputVarType } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
const i18nPrefix = 'nodes.fileUpload'
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
id: string
|
||||||
|
payload: FileUploadNodeType
|
||||||
|
runInputData: Record<string, unknown>
|
||||||
|
runInputDataRef: RefObject<Record<string, unknown>>
|
||||||
|
getInputVars: (textList: string[]) => InputVar[]
|
||||||
|
setRunInputData: (data: Record<string, unknown>) => void
|
||||||
|
toVarInputs: (variables: Variable[]) => InputVar[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSingleRunFormParams = ({
|
||||||
|
payload,
|
||||||
|
runInputData,
|
||||||
|
setRunInputData,
|
||||||
|
}: Params) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const files = runInputData.files
|
||||||
|
|
||||||
|
const setFiles = useCallback((newFiles: []) => {
|
||||||
|
setRunInputData({
|
||||||
|
...runInputData,
|
||||||
|
files: newFiles,
|
||||||
|
})
|
||||||
|
}, [runInputData, setRunInputData])
|
||||||
|
|
||||||
|
const forms = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
inputs: [{
|
||||||
|
label: t(`${i18nPrefix}.inputVar`, { ns: 'workflow' })!,
|
||||||
|
variable: 'files',
|
||||||
|
type: payload.is_array_file ? InputVarType.multiFiles : InputVarType.singleFile,
|
||||||
|
required: true,
|
||||||
|
}],
|
||||||
|
values: { files },
|
||||||
|
onChange: (keyValue: Record<string, unknown>) => setFiles((keyValue.files as []) || []),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [files, payload.is_array_file, setFiles, t])
|
||||||
|
|
||||||
|
const getDependentVars = () => {
|
||||||
|
return [payload.variable_selector]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDependentVar = (variable: string) => {
|
||||||
|
if (variable === 'files')
|
||||||
|
return payload.variable_selector
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
forms,
|
||||||
|
getDependentVars,
|
||||||
|
getDependentVar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSingleRunFormParams
|
||||||
@ -59,6 +59,7 @@ export enum BlockEnum {
|
|||||||
TriggerWebhook = 'trigger-webhook',
|
TriggerWebhook = 'trigger-webhook',
|
||||||
TriggerPlugin = 'trigger-plugin',
|
TriggerPlugin = 'trigger-plugin',
|
||||||
Command = 'command',
|
Command = 'command',
|
||||||
|
FileUpload = 'file-upload',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ControlMode {
|
export enum ControlMode {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export const canRunBySingle = (nodeType: BlockEnum, isChildNode: boolean) => {
|
|||||||
|| nodeType === BlockEnum.KnowledgeRetrieval
|
|| nodeType === BlockEnum.KnowledgeRetrieval
|
||||||
|| nodeType === BlockEnum.Code
|
|| nodeType === BlockEnum.Code
|
||||||
|| nodeType === BlockEnum.Command
|
|| nodeType === BlockEnum.Command
|
||||||
|
|| nodeType === BlockEnum.FileUpload
|
||||||
|| nodeType === BlockEnum.TemplateTransform
|
|| nodeType === BlockEnum.TemplateTransform
|
||||||
|| nodeType === BlockEnum.QuestionClassifier
|
|| nodeType === BlockEnum.QuestionClassifier
|
||||||
|| nodeType === BlockEnum.HttpRequest
|
|| nodeType === BlockEnum.HttpRequest
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"blocks.datasource-empty": "Empty Data Source",
|
"blocks.datasource-empty": "Empty Data Source",
|
||||||
"blocks.document-extractor": "Doc Extractor",
|
"blocks.document-extractor": "Doc Extractor",
|
||||||
"blocks.end": "Output",
|
"blocks.end": "Output",
|
||||||
|
"blocks.file-upload": "File Upload",
|
||||||
"blocks.group": "Group",
|
"blocks.group": "Group",
|
||||||
"blocks.http-request": "HTTP Request",
|
"blocks.http-request": "HTTP Request",
|
||||||
"blocks.human-input": "Human Input",
|
"blocks.human-input": "Human Input",
|
||||||
@ -41,6 +42,7 @@
|
|||||||
"blocksAbout.datasource-empty": "Empty Data Source placeholder",
|
"blocksAbout.datasource-empty": "Empty Data Source placeholder",
|
||||||
"blocksAbout.document-extractor": "Used to parse uploaded documents into text content that is easily understandable by LLM.",
|
"blocksAbout.document-extractor": "Used to parse uploaded documents into text content that is easily understandable by LLM.",
|
||||||
"blocksAbout.end": "Define the output and result type of a workflow",
|
"blocksAbout.end": "Define the output and result type of a workflow",
|
||||||
|
"blocksAbout.file-upload": "Download selected file variables into sandbox as local paths",
|
||||||
"blocksAbout.group": "Group multiple nodes together for better organization",
|
"blocksAbout.group": "Group multiple nodes together for better organization",
|
||||||
"blocksAbout.http-request": "Allow server requests to be sent over the HTTP protocol",
|
"blocksAbout.http-request": "Allow server requests to be sent over the HTTP protocol",
|
||||||
"blocksAbout.human-input": "Ask for human to confirm before generating the next step",
|
"blocksAbout.human-input": "Ask for human to confirm before generating the next step",
|
||||||
@ -341,6 +343,7 @@
|
|||||||
"errorMsg.fieldRequired": "{{field}} is required",
|
"errorMsg.fieldRequired": "{{field}} is required",
|
||||||
"errorMsg.fields.code": "Code",
|
"errorMsg.fields.code": "Code",
|
||||||
"errorMsg.fields.command": "Command",
|
"errorMsg.fields.command": "Command",
|
||||||
|
"errorMsg.fields.fileVariable": "File Variable",
|
||||||
"errorMsg.fields.model": "Model",
|
"errorMsg.fields.model": "Model",
|
||||||
"errorMsg.fields.rerankModel": "A configured Rerank Model",
|
"errorMsg.fields.rerankModel": "A configured Rerank Model",
|
||||||
"errorMsg.fields.variable": "Variable Name",
|
"errorMsg.fields.variable": "Variable Name",
|
||||||
@ -515,6 +518,9 @@
|
|||||||
"nodes.end.type.none": "None",
|
"nodes.end.type.none": "None",
|
||||||
"nodes.end.type.plain-text": "Plain Text",
|
"nodes.end.type.plain-text": "Plain Text",
|
||||||
"nodes.end.type.structured": "Structured",
|
"nodes.end.type.structured": "Structured",
|
||||||
|
"nodes.fileUpload.inputVar": "File Variable",
|
||||||
|
"nodes.fileUpload.outputVars.fileName": "File name in sandbox",
|
||||||
|
"nodes.fileUpload.outputVars.sandboxPath": "Sandbox local file path",
|
||||||
"nodes.http.api": "API",
|
"nodes.http.api": "API",
|
||||||
"nodes.http.apiPlaceholder": "Enter URL, type ‘/’ insert variable",
|
"nodes.http.apiPlaceholder": "Enter URL, type ‘/’ insert variable",
|
||||||
"nodes.http.authorization.api-key": "API-Key",
|
"nodes.http.authorization.api-key": "API-Key",
|
||||||
|
|||||||
Reference in New Issue
Block a user