feat: (trigger) support file upload in webhook (#25159)

This commit is contained in:
非法操作 2025-09-04 18:33:42 +08:00 committed by GitHub
parent e751c0c535
commit 461829274a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 102 additions and 6 deletions

View File

@ -176,6 +176,7 @@ class WebhookTriggerApi(Resource):
tenant_id=current_user.current_tenant_id,
webhook_id=webhook_id,
triggered_by=triggered_by,
created_by=current_user.id,
)
session.add(webhook_trigger)

View File

@ -26,6 +26,7 @@ def upgrade():
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
sa.Column('webhook_id', sa.String(length=24), nullable=False),
sa.Column('triggered_by', sa.String(length=16), nullable=False),
sa.Column('created_by', models.types.StringUUID(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id', name='workflow_webhook_trigger_pkey'),

View File

@ -1397,6 +1397,7 @@ class WorkflowWebhookTrigger(Base):
- tenant_id (uuid) Workspace ID
- webhook_id (varchar) Webhook ID for URL: https://api.dify.ai/triggers/webhook/:webhook_id
- triggered_by (varchar) Environment: debugger or production
- created_by (varchar) User ID of the creator
- created_at (timestamp) Creation time
- updated_at (timestamp) Last update time
"""
@ -1415,6 +1416,7 @@ class WorkflowWebhookTrigger(Base):
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
webhook_id: Mapped[str] = mapped_column(String(24), nullable=False)
triggered_by: Mapped[str] = mapped_column(String(16), nullable=False)
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
updated_at: Mapped[datetime] = mapped_column(
DateTime,

View File

@ -119,7 +119,7 @@ class WebhookService:
# Create file using ToolFileManager
tool_file = tool_file_manager.create_file_by_raw(
user_id="webhook_user",
user_id=webhook_trigger.created_by,
tenant_id=webhook_trigger.tenant_id,
conversation_id=None,
file_binary=file_content,
@ -135,8 +135,7 @@ class WebhookService:
mapping=mapping,
tenant_id=webhook_trigger.tenant_id,
)
processed_files[name] = file_obj
processed_files[name] = file_obj.to_dict()
except Exception:
logger.exception("Failed to process file upload %s", name)
@ -164,6 +163,10 @@ class WebhookService:
request_content_type = webhook_data["headers"].get("Content-Type", "").lower()
if not request_content_type:
request_content_type = webhook_data["headers"].get("content-type", "application/json").lower()
# Extract the main content type (ignore parameters like boundary)
request_content_type = request_content_type.split(";")[0].strip()
if configured_content_type != request_content_type:
return {
"valid": False,
@ -221,9 +224,58 @@ class WebhookService:
if not validation_result["valid"]:
return validation_result
elif configured_content_type == "application/x-www-form-urlencoded":
# For form-urlencoded data, all values must be strings - no other types allowed
body_params = node_data.get("body", [])
body_data = webhook_data.get("body", {})
for body_param in body_params:
param_name = body_param.get("name", "")
param_type = body_param.get("type", SegmentType.STRING)
is_required = body_param.get("required", False)
param_exists = param_name in body_data
if is_required and not param_exists:
return {"valid": False, "error": f"Required body parameter missing: {param_name}"}
# Ensure the actual value is also a string
if param_exists and param_type != SegmentType.STRING:
param_value = body_data[param_name]
validation_result = cls._validate_form_parameter_type(param_name, param_value, param_type)
if not validation_result["valid"]:
return validation_result
elif configured_content_type == "multipart/form-data":
# For multipart data, supports both strings and files
body_params = node_data.get("body", [])
body_data = webhook_data.get("body", {})
for body_param in body_params:
param_name = body_param.get("name", "")
param_type = body_param.get("type", SegmentType.STRING)
is_required = body_param.get("required", False)
if param_type == SegmentType.FILE:
# File parameters are handled separately in files dict
file_obj = webhook_data.get("files", {}).get(param_name)
if is_required and not file_obj:
return {"valid": False, "error": f"Required file parameter missing: {param_name}"}
else:
# Multipart form data parameters are all strings
param_exists = param_name in body_data
if is_required and not param_exists:
return {"valid": False, "error": f"Required body parameter missing: {param_name}"}
# For form data, validate that non-string types can be converted
if param_exists and param_type != SegmentType.STRING:
param_value = body_data[param_name]
validation_result = cls._validate_form_parameter_type(param_name, param_value, param_type)
if not validation_result["valid"]:
return validation_result
else:
# For other content types (multipart/form-data, application/x-www-form-urlencoded, etc.)
# Only validate existence of required parameters, no type validation
# For other unsupported content types, only validate existence of required parameters
body_params = node_data.get("body", [])
for body_param in body_params:
param_name = body_param.get("name", "")
@ -327,6 +379,47 @@ class WebhookService:
logger.exception("Type validation error for parameter %s", param_name)
return {"valid": False, "error": f"Type validation failed for parameter '{param_name}'"}
@classmethod
def _validate_form_parameter_type(cls, param_name: str, param_value: str, param_type: str) -> dict[str, Any]:
"""Validate form parameter type against expected type. Form data are always strings but can be converted."""
try:
# Form data values are always strings, but we can validate if they can be interpreted as other types
if param_type == SegmentType.STRING:
# String is always valid
return {"valid": True}
elif param_type == SegmentType.NUMBER:
# Check if string can be converted to number
try:
float(param_value)
return {"valid": True}
except ValueError:
return {
"valid": False,
"error": f"Parameter '{param_name}' must be a valid number, got '{param_value}'",
}
elif param_type == SegmentType.BOOLEAN:
# Check if string represents a boolean
if param_value.lower() in ["true", "false", "1", "0", "yes", "no"]:
return {"valid": True}
else:
return {
"valid": False,
"error": f"Parameter '{param_name}' must be a boolean value, got '{param_value}'",
}
else:
# For other types (object, arrays), form data is not suitable
return {
"valid": False,
"error": f"Parameter '{param_name}' type '{param_type}' is not supported for form data.",
}
except Exception:
logger.exception("Form type validation error for parameter %s", param_name)
return {"valid": False, "error": f"Form type validation failed for parameter '{param_name}'"}
@classmethod
def trigger_workflow_execution(
cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: dict[str, Any], workflow: Workflow
@ -355,7 +448,6 @@ class WebhookService:
"webhook_headers": webhook_data.get("headers", {}),
"webhook_query_params": webhook_data.get("query_params", {}),
"webhook_body": webhook_data.get("body", {}),
"webhook_files": webhook_data.get("files", {}),
}
# Create trigger data