Files
非法操作andautofix-ci[bot] 163062847b refactor(api): relocate trigger plugin runtime (#45966)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-11-11 09:43:32 +00:00

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 constants.trigger import TRIGGER_PLUGIN_NODE_TYPE, TRIGGER_SCHEDULE_NODE_TYPE, TRIGGER_WEBHOOK_NODE_TYPE
from core.plugin.entities.request import TriggerInvokeEventResponse
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"])