mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 05:41:09 +08:00
148 lines
6.5 KiB
Python
148 lines
6.5 KiB
Python
"""Poll trigger debug inputs using injected event and schedule runtime stores."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from collections.abc import Callable, Iterable, Mapping
|
|
from datetime import datetime
|
|
from typing import Any, Protocol
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from core.plugin.entities.request import TriggerInvokeEventResponse
|
|
from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE, TRIGGER_SCHEDULE_NODE_TYPE, TRIGGER_WEBHOOK_NODE_TYPE
|
|
from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData
|
|
from graphon.entities.graph_config import NodeConfigDict
|
|
from libs.datetime_utils import ensure_naive_utc
|
|
from libs.schedule_utils import calculate_next_run_at
|
|
from models.provider_ids import TriggerProviderID
|
|
from services.trigger.debug_events import (
|
|
PluginTriggerDebugEvent,
|
|
WebhookDebugEvent,
|
|
build_plugin_pool_key,
|
|
build_webhook_pool_key,
|
|
)
|
|
from services.trigger.debug_store import ScheduleDebugRuntime, ScheduleDebugRuntimeStore, TriggerDebugEventStore
|
|
from services.trigger.schedule_service import ScheduleService
|
|
from services.trigger.webhook_service import build_webhook_workflow_inputs
|
|
|
|
|
|
class TriggerDebugEvent(BaseModel):
|
|
workflow_args: Mapping[str, Any]
|
|
node_id: str
|
|
|
|
|
|
class PluginTriggerEventInvoker(Protocol):
|
|
def __call__(
|
|
self, *, tenant_id: str, user_id: str, node_config: NodeConfigDict, event: PluginTriggerDebugEvent
|
|
) -> TriggerInvokeEventResponse: ...
|
|
|
|
|
|
class TriggerDebugService:
|
|
"""Route debug events and schedule ticks through injected runtime stores."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
events: TriggerDebugEventStore,
|
|
schedules: ScheduleDebugRuntimeStore,
|
|
invoke_plugin_event: PluginTriggerEventInvoker,
|
|
now: Callable[[], datetime],
|
|
) -> None:
|
|
self._events = events
|
|
self._schedules = schedules
|
|
self._invoke_plugin_event = invoke_plugin_event
|
|
self._now = now
|
|
|
|
def dispatch_webhook_debug(self, *, tenant_id: str, app_id: str, event: WebhookDebugEvent) -> int:
|
|
"""Deliver an event to waiting listeners for its tenant, app, and node."""
|
|
pool_key = build_webhook_pool_key(tenant_id, app_id, event.node_id)
|
|
return self._events.dispatch(tenant_id=tenant_id, event=event, pool_key=pool_key)
|
|
|
|
def poll(
|
|
self, *, node_config: NodeConfigDict, tenant_id: str, user_id: str, app_id: str
|
|
) -> TriggerDebugEvent | None:
|
|
"""Poll a supported trigger node for this user; return None when no event is ready.
|
|
|
|
Unsupported node types raise ValueError; store and plugin errors propagate.
|
|
"""
|
|
node_type = node_config["data"].type
|
|
node_id = node_config["id"]
|
|
if node_type == TRIGGER_PLUGIN_NODE_TYPE:
|
|
return self._poll_plugin(node_config=node_config, tenant_id=tenant_id, user_id=user_id, app_id=app_id)
|
|
if node_type == TRIGGER_WEBHOOK_NODE_TYPE:
|
|
event = self._events.poll(
|
|
event_type=WebhookDebugEvent,
|
|
pool_key=build_webhook_pool_key(tenant_id, app_id, node_id),
|
|
tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
app_id=app_id,
|
|
node_id=node_id,
|
|
)
|
|
if event is None:
|
|
return None
|
|
inputs = event.payload.get("inputs")
|
|
if inputs is None:
|
|
inputs = build_webhook_workflow_inputs(event.payload.get("webhook_data", {}))
|
|
return TriggerDebugEvent(workflow_args={"inputs": inputs or {}, "files": []}, node_id=node_id)
|
|
if node_type == TRIGGER_SCHEDULE_NODE_TYPE:
|
|
return self._poll_schedule(node_config=node_config, tenant_id=tenant_id, user_id=user_id, app_id=app_id)
|
|
raise ValueError(f"unable to create event poller for node type {node_type}")
|
|
|
|
def select_events(
|
|
self, *, node_configs: Iterable[NodeConfigDict], tenant_id: str, user_id: str, app_id: str
|
|
) -> TriggerDebugEvent | None:
|
|
"""Poll in graph-request order and stop after the first available event."""
|
|
for node_config in node_configs:
|
|
event = self.poll(node_config=node_config, tenant_id=tenant_id, user_id=user_id, app_id=app_id)
|
|
if event is not None:
|
|
return event
|
|
return None
|
|
|
|
def _poll_plugin(
|
|
self, *, node_config: NodeConfigDict, tenant_id: str, user_id: str, app_id: str
|
|
) -> TriggerDebugEvent | None:
|
|
"""Consume and invoke a plugin event; cancelled or absent events return None."""
|
|
node = TriggerEventNodeData.model_validate(node_config["data"], from_attributes=True)
|
|
pool_key = build_plugin_pool_key(
|
|
tenant_id, str(TriggerProviderID(node.provider_id)), node.subscription_id, node.event_name
|
|
)
|
|
event = self._events.poll(
|
|
event_type=PluginTriggerDebugEvent,
|
|
pool_key=pool_key,
|
|
tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
app_id=app_id,
|
|
node_id=node_config["id"],
|
|
)
|
|
if event is None:
|
|
return None
|
|
response = self._invoke_plugin_event(
|
|
tenant_id=tenant_id, user_id=event.user_id, node_config=node_config, event=event
|
|
)
|
|
if response.cancelled:
|
|
return None
|
|
return TriggerDebugEvent(workflow_args={"inputs": response.variables, "files": []}, node_id=node_config["id"])
|
|
|
|
def _poll_schedule(
|
|
self, *, node_config: NodeConfigDict, tenant_id: str, user_id: str, app_id: str
|
|
) -> TriggerDebugEvent | None:
|
|
"""Initialize or consume a user's schedule tick according to its stored next run time."""
|
|
config = ScheduleService.to_schedule_config(node_config)
|
|
cron_hash = hashlib.sha256(config.cron_expression.encode()).hexdigest()
|
|
cache_key = f"schedule_debug_runtime:{tenant_id}:{user_id}:{app_id}:{node_config['id']}:{cron_hash}"
|
|
runtime = self._schedules.get_runtime(cache_key)
|
|
now = self._now()
|
|
if runtime is None:
|
|
runtime = ScheduleDebugRuntime(
|
|
cache_key=cache_key,
|
|
timezone=config.timezone,
|
|
cron_expression=config.cron_expression,
|
|
next_run_at=ensure_naive_utc(calculate_next_run_at(config.cron_expression, config.timezone, now)),
|
|
)
|
|
self._schedules.save_runtime(runtime)
|
|
if ensure_naive_utc(runtime.next_run_at) > ensure_naive_utc(now):
|
|
return None
|
|
self._schedules.delete_runtime(cache_key)
|
|
return TriggerDebugEvent(workflow_args={"inputs": {}, "files": []}, node_id=node_config["id"])
|