feat(trigger): enhance trigger event handling and introduce new debug event polling

- Refactored the `DraftWorkflowTriggerNodeApi` and related services to utilize the new `TriggerService` for polling debug events, improving modularity and clarity.
- Added `poll_debug_event` methods in `TriggerService`, `ScheduleService`, and `WebhookService` to streamline event handling for different trigger types.
- Introduced `ScheduleDebugEvent` and updated `PluginTriggerDebugEvent` to include a more structured approach for event data.
- Enhanced the `invoke_trigger_event` method to improve error handling and data validation during trigger invocations.
- Updated frontend API calls to align with the new event structure, removing deprecated parameters for cleaner integration.
This commit is contained in:
Harry
2025-10-15 01:18:06 +08:00
parent b20f61356c
commit dab4e521af
14 changed files with 359 additions and 139 deletions

View File

@ -11,12 +11,29 @@ from core.workflow.nodes.trigger_schedule.entities import ScheduleConfig, Schedu
from core.workflow.nodes.trigger_schedule.exc import ScheduleConfigError, ScheduleNotFoundError
from libs.schedule_utils import calculate_next_run_at, convert_12h_to_24h
from models.account import Account, TenantAccountJoin
from models.model import App
from models.workflow import Workflow, WorkflowSchedulePlan
from services.trigger.trigger_debug_service import ScheduleDebugEvent, TriggerDebugService
logger = logging.getLogger(__name__)
class ScheduleService:
@classmethod
def poll_debug_event(cls, app_model: App, user_id: str, node_id: str) -> ScheduleDebugEvent | None:
"""Poll a debug event for a schedule trigger."""
pool_key = ScheduleDebugEvent.build_pool_key(
tenant_id=app_model.tenant_id, app_id=app_model.id, node_id=node_id
)
return TriggerDebugService.poll(
event_type=ScheduleDebugEvent,
pool_key=pool_key,
tenant_id=app_model.tenant_id,
user_id=user_id,
app_id=app_model.id,
node_id=node_id,
)
@staticmethod
def create_schedule(
session: Session,

View File

@ -3,7 +3,8 @@
import hashlib
import logging
from abc import ABC, abstractmethod
from typing import Any, Optional, TypeVar
from collections.abc import Mapping
from typing import Any, TypeVar
from pydantic import BaseModel, Field
from redis import RedisError
@ -39,24 +40,25 @@ class BaseDebugEvent(ABC, BaseModel):
class PluginTriggerDebugEvent(BaseDebugEvent):
"""Debug event for plugin triggers."""
name: str
request_id: str
subscription_id: str
event_name: str
provider_id: str
@classmethod
def build_pool_key(cls, **kwargs: Any) -> str:
"""Generate pool key for plugin trigger events.
Args:
name: Event name
tenant_id: Tenant ID
provider_id: Provider ID
subscription_id: Subscription ID
event_name: Event name
"""
tenant_id = kwargs["tenant_id"]
provider_id = kwargs["provider_id"]
subscription_id = kwargs["subscription_id"]
event_name = kwargs["event_name"]
event_name = kwargs["name"]
return f"plugin_trigger_debug_waiting_pool:{tenant_id}:{str(provider_id)}:{subscription_id}:{event_name}"
@ -82,6 +84,27 @@ class WebhookDebugEvent(BaseDebugEvent):
return f"webhook_trigger_debug_waiting_pool:{tenant_id}:{app_id}:{node_id}"
class ScheduleDebugEvent(BaseDebugEvent):
"""Debug event for schedule triggers."""
node_id: str
inputs: Mapping[str, Any]
@classmethod
def build_pool_key(cls, **kwargs: Any) -> str:
"""Generate pool key for schedule events.
Args:
tenant_id: Tenant ID
app_id: App ID
node_id: Node ID
"""
tenant_id = kwargs["tenant_id"]
app_id = kwargs["app_id"]
node_id = kwargs["node_id"]
return f"schedule_trigger_debug_waiting_pool:{tenant_id}:{app_id}:{node_id}"
class TriggerDebugService:
"""
Unified Redis-based trigger debug service with polling support.
@ -157,7 +180,7 @@ class TriggerDebugService:
user_id: str,
app_id: str,
node_id: str,
) -> Optional[TEvent]:
) -> TEvent | None:
"""
Poll for an event or register to the waiting pool.

View File

@ -192,9 +192,7 @@ class TriggerProviderService:
raise ValueError(str(e))
@classmethod
def get_subscription_by_id(
cls, tenant_id: str, subscription_id: str | None = None
) -> TriggerProviderSubscriptionApiEntity | None:
def get_subscription_by_id(cls, tenant_id: str, subscription_id: str | None = None) -> TriggerSubscription | None:
"""
Get a trigger subscription by the ID.
"""
@ -216,7 +214,13 @@ class TriggerProviderService:
subscription=subscription,
)
subscription.credentials = encrypter.decrypt(subscription.credentials)
return subscription.to_api_entity()
properties_encrypter, _ = create_trigger_provider_encrypter_for_properties(
tenant_id=subscription.tenant_id,
controller=provider_controller,
subscription=subscription,
)
subscription.properties = properties_encrypter.decrypt(subscription.properties)
return subscription
return None
@classmethod

View File

@ -2,11 +2,13 @@ import logging
import time
import uuid
from collections.abc import Mapping, Sequence
from typing import Any
from flask import Request, Response
from flask import Request, Response, request
from sqlalchemy import and_, func, select
from sqlalchemy.orm import Session
from core.helper.trace_id_helper import get_external_trace_id
from core.plugin.entities.plugin_daemon import CredentialType
from core.plugin.entities.request import TriggerDispatchResponse, TriggerInvokeEventResponse
from core.plugin.utils.http_parser import deserialize_request, serialize_request
@ -20,12 +22,15 @@ from extensions.ext_database import db
from extensions.ext_storage import storage
from models.account import Account, TenantAccountJoin, TenantAccountRole
from models.enums import WorkflowRunTriggeredFrom
from models.model import App
from models.provider_ids import TriggerProviderID
from models.trigger import TriggerSubscription
from models.workflow import AppTrigger, AppTriggerStatus, Workflow, WorkflowPluginTrigger
from services.async_workflow_service import AsyncWorkflowService
from services.trigger.trigger_debug_service import PluginTriggerDebugEvent, TriggerDebugService
from services.trigger.trigger_provider_service import TriggerProviderService
from services.workflow.entities import PluginTriggerData, PluginTriggerDispatchData
from services.workflow_service import WorkflowService
logger = logging.getLogger(__name__)
@ -35,7 +40,86 @@ class TriggerService:
__ENDPOINT_REQUEST_CACHE_COUNT__ = 10
__ENDPOINT_REQUEST_CACHE_EXPIRE_MS__ = 5 * 60 * 1000
__WEBHOOK_NODE_CACHE_KEY__ = "webhook_nodes"
@classmethod
def invoke_trigger_event(
cls, tenant_id: str, user_id: str, node_config: Mapping[str, Any], event: PluginTriggerDebugEvent
) -> TriggerInvokeEventResponse:
"""Invoke a trigger event."""
subscription: TriggerSubscription | None = TriggerProviderService.get_subscription_by_id(
tenant_id=tenant_id,
subscription_id=event.subscription_id,
)
if not subscription:
raise ValueError("Subscription not found")
node_data = node_config.get("data")
if not node_data:
raise ValueError("Node data not found")
request = deserialize_request(storage.load_once(f"triggers/{event.request_id}"))
if not request:
raise ValueError("Request not found")
# invoke triger
return TriggerManager.invoke_trigger_event(
tenant_id=tenant_id,
user_id=user_id,
provider_id=TriggerProviderID(event.provider_id),
event_name=event.name,
parameters=node_data.get("parameters", {}),
credentials=subscription.credentials,
credential_type=CredentialType.of(subscription.credential_type),
subscription=subscription.to_entity(),
request=request,
)
@classmethod
def build_workflow_args(cls, event: PluginTriggerDebugEvent) -> Mapping[str, Any]:
"""Build workflow args from plugin trigger debug event."""
workflow_args = {
"inputs": event.model_dump(),
"query": "",
"files": [],
}
external_trace_id = get_external_trace_id(request)
if external_trace_id:
workflow_args["external_trace_id"] = external_trace_id
return workflow_args
@classmethod
def poll_debug_event(cls, app_model: App, user_id: str, node_id: str) -> PluginTriggerDebugEvent | None:
"""Poll webhook debug event for a given node ID."""
workflow_service = WorkflowService()
workflow: Workflow | None = workflow_service.get_draft_workflow(
app_model=app_model,
workflow_id=None,
)
if not workflow:
raise ValueError("Workflow not found")
node_data = workflow.get_node_config_by_id(node_id=node_id).get("data")
if not node_data:
raise ValueError("Node config not found")
event_name = node_data.get("event_name")
subscription_id = node_data.get("subscription_id")
if not subscription_id:
raise ValueError("Subscription ID not found")
provider_id = TriggerProviderID(node_data.get("provider_id"))
pool_key: str = PluginTriggerDebugEvent.build_pool_key(
name=event_name,
provider_id=provider_id,
tenant_id=app_model.tenant_id,
subscription_id=subscription_id,
)
return TriggerDebugService.poll(
event_type=PluginTriggerDebugEvent,
pool_key=pool_key,
tenant_id=app_model.tenant_id,
user_id=user_id,
app_id=app_model.id,
node_id=node_id,
)
@classmethod
def _get_latest_workflows_by_app_ids(
@ -129,7 +213,7 @@ class TriggerService:
user_id=subscription.user_id,
provider_id=TriggerProviderID(subscription.provider_id),
event_name=event.identity.name,
parameters=event_node.get("config", {}),
parameters=event_node.get("config", {}).get("parameters", {}),
credentials=subscription.credentials,
credential_type=CredentialType.of(subscription.credential_type),
subscription=subscription.to_entity(),

View File

@ -13,6 +13,7 @@ from werkzeug.exceptions import RequestEntityTooLarge
from configs import dify_config
from core.file.models import FileTransferMethod
from core.helper.trace_id_helper import get_external_trace_id
from core.tools.tool_file_manager import ToolFileManager
from core.variables.types import SegmentType
from core.workflow.enums import NodeType
@ -24,6 +25,7 @@ from models.enums import WorkflowRunTriggeredFrom
from models.model import App
from models.workflow import AppTrigger, AppTriggerStatus, AppTriggerType, Workflow, WorkflowWebhookTrigger
from services.async_workflow_service import AsyncWorkflowService
from services.trigger.trigger_debug_service import TriggerDebugService, WebhookDebugEvent
from services.workflow.entities import TriggerData
logger = logging.getLogger(__name__)
@ -35,6 +37,53 @@ class WebhookService:
__WEBHOOK_NODE_CACHE_KEY__ = "webhook_nodes"
MAX_WEBHOOK_NODES_PER_WORKFLOW = 5 # Maximum allowed webhook nodes per workflow
@classmethod
def build_workflow_args(cls, event: WebhookDebugEvent) -> Mapping[str, Any]:
"""Build workflow args from webhook debug event."""
payload = event.payload or {}
workflow_inputs = payload.get("inputs")
if workflow_inputs is None:
webhook_data = payload.get("webhook_data", {})
workflow_inputs = WebhookService.build_workflow_inputs(webhook_data)
workflow_args = {
"inputs": workflow_inputs or {},
"query": "",
"files": [],
}
external_trace_id = get_external_trace_id(request)
if external_trace_id:
workflow_args["external_trace_id"] = external_trace_id
return workflow_args
@classmethod
def poll_debug_event(cls, app_model: App, user_id: str, node_id: str) -> WebhookDebugEvent | None:
"""Poll webhook debug event for a given node ID.
Args:
app_model: The app model
user_id: The user ID
node_id: The node ID to poll for
Returns:
WebhookDebugEvent | None: The webhook debug event if available, None otherwise
"""
pool_key = WebhookDebugEvent.build_pool_key(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
node_id=node_id,
)
return TriggerDebugService.poll(
event_type=WebhookDebugEvent,
pool_key=pool_key,
tenant_id=app_model.tenant_id,
user_id=user_id,
app_id=app_model.id,
node_id=node_id,
)
@classmethod
def get_webhook_trigger_and_workflow(
cls, webhook_id: str, is_debug: bool = False