mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 13:51:25 +08:00
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
"""Native webhook admission and execution, independent of HTTP and SQL sessions."""
|
|
|
|
from collections.abc import Callable, Sequence
|
|
from dataclasses import dataclass
|
|
from typing import Any, Protocol, TypedDict
|
|
|
|
from enums import QuotaType
|
|
from graphon.entities.graph_config import NodeConfigDict
|
|
from graphon.file import File
|
|
from models.enums import AppTriggerStatus
|
|
from services.async_workflow_service import TriggerWorkflowDispatcher
|
|
from services.errors.app import QuotaExceededError
|
|
from services.quota_service import QuotaCharge
|
|
from services.trigger.workflow_dispatch_service import TriggerEndUserProvisioner
|
|
from services.workflow.entities import TriggerActor, WebhookTriggerData
|
|
|
|
|
|
class WebhookNotFoundError(Exception):
|
|
"""The webhook or its admitted workflow is unavailable."""
|
|
|
|
|
|
class RawWebhookDataDict(TypedDict):
|
|
method: str
|
|
headers: dict[str, str]
|
|
query_params: dict[str, Any]
|
|
body: dict[str, Any]
|
|
files: dict[str, Any]
|
|
|
|
|
|
class WorkflowInputsDict(TypedDict):
|
|
webhook_data: RawWebhookDataDict
|
|
webhook_headers: dict[str, str]
|
|
webhook_query_params: dict[str, Any]
|
|
webhook_body: dict[str, Any]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class WebhookContext:
|
|
tenant_id: str
|
|
app_id: str
|
|
webhook_id: str
|
|
node_id: str
|
|
created_by: str
|
|
workflow_id: str
|
|
node_config: NodeConfigDict
|
|
# Draft lookup does not require a published AppTrigger record.
|
|
status: AppTriggerStatus | None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class UnavailableWebhook:
|
|
"""Inactive published triggers are admitted before loading workflow configuration."""
|
|
|
|
tenant_id: str
|
|
status: AppTriggerStatus
|
|
|
|
|
|
class WebhookQuery(Protocol):
|
|
def get_webhook_context(self, *, webhook_id: str, debug: bool) -> WebhookContext | UnavailableWebhook: ...
|
|
|
|
|
|
class WebhookFiles(Protocol):
|
|
def create(self, *, tenant_id: str, user_id: str, content: bytes, mimetype: str) -> File: ...
|
|
|
|
|
|
class WebhookBindingWriter(Protocol):
|
|
def __call__(self, *, tenant_id: str, app_id: str, created_by: str | None, node_ids: Sequence[str]) -> None: ...
|
|
|
|
|
|
def build_webhook_workflow_inputs(webhook_data: RawWebhookDataDict) -> WorkflowInputsDict:
|
|
return {
|
|
"webhook_data": webhook_data,
|
|
"webhook_headers": webhook_data.get("headers", {}),
|
|
"webhook_query_params": webhook_data.get("query_params", {}),
|
|
"webhook_body": webhook_data.get("body", {}),
|
|
}
|
|
|
|
|
|
class WebhookBindingService:
|
|
MAX_WEBHOOK_NODES_PER_WORKFLOW = 5
|
|
|
|
def __init__(self, *, sync_bindings: WebhookBindingWriter) -> None:
|
|
self._sync_bindings = sync_bindings
|
|
|
|
def sync(self, *, tenant_id: str, app_id: str, created_by: str | None, node_ids: Sequence[str]) -> None:
|
|
if len(node_ids) > self.MAX_WEBHOOK_NODES_PER_WORKFLOW:
|
|
raise ValueError(
|
|
"Workflow exceeds maximum webhook node limit. "
|
|
f"Found {len(node_ids)} webhook nodes, maximum allowed is {self.MAX_WEBHOOK_NODES_PER_WORKFLOW}"
|
|
)
|
|
self._sync_bindings(tenant_id=tenant_id, app_id=app_id, created_by=created_by, node_ids=node_ids)
|
|
|
|
|
|
class WebhookService:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
webhooks: WebhookQuery,
|
|
end_users: TriggerEndUserProvisioner,
|
|
dispatch: TriggerWorkflowDispatcher,
|
|
reserve_quota: Callable[[QuotaType, str], QuotaCharge],
|
|
mark_rate_limited: Callable[[str], None],
|
|
) -> None:
|
|
self._webhooks = webhooks
|
|
self._end_users = end_users
|
|
self._dispatch = dispatch
|
|
self._reserve_quota = reserve_quota
|
|
self._mark_rate_limited = mark_rate_limited
|
|
|
|
def prepare(self, *, webhook_id: str, debug: bool) -> WebhookContext:
|
|
context = self._webhooks.get_webhook_context(webhook_id=webhook_id, debug=debug)
|
|
if isinstance(context, UnavailableWebhook):
|
|
if context.status == AppTriggerStatus.RATE_LIMITED:
|
|
raise QuotaExceededError(feature=QuotaType.TRIGGER.value, tenant_id=context.tenant_id, required=1)
|
|
raise WebhookNotFoundError(f"Webhook trigger is disabled for webhook {webhook_id}")
|
|
return context
|
|
|
|
def resolve_actor(self, *, context: WebhookContext) -> TriggerActor:
|
|
return self._end_users.get_or_create_trigger_end_user(tenant_id=context.tenant_id, app_id=context.app_id)
|
|
|
|
def execute(
|
|
self,
|
|
*,
|
|
context: WebhookContext,
|
|
data: RawWebhookDataDict,
|
|
actor: TriggerActor | None = None,
|
|
) -> None:
|
|
resolved_actor = actor or self.resolve_actor(context=context)
|
|
try:
|
|
charge = self._reserve_quota(QuotaType.TRIGGER, context.tenant_id)
|
|
except QuotaExceededError:
|
|
self._mark_rate_limited(context.tenant_id)
|
|
raise
|
|
try:
|
|
self._dispatch.trigger_workflow_async(
|
|
actor=resolved_actor,
|
|
trigger_data=WebhookTriggerData(
|
|
tenant_id=context.tenant_id,
|
|
app_id=context.app_id,
|
|
workflow_id=context.workflow_id,
|
|
root_node_id=context.node_id,
|
|
inputs=build_webhook_workflow_inputs(data),
|
|
),
|
|
)
|
|
charge.commit()
|
|
except Exception:
|
|
charge.refund()
|
|
raise
|