feat(trigger): enhance trigger execution and response handling

- Introduced new response entities for trigger operations: TriggerInvokeResponse, TriggerDispatchResponse, TriggerSubscriptionResponse, TriggerUnsubscribeResponse, and TriggerRefreshResponse.
- Updated PluginExecutor methods to utilize the new response structures, improving clarity and consistency in trigger handling.
- Enhanced HTTP request parsing and response conversion utilities for better integration with various content types.
- Refactored GitHub provider to align with the updated trigger dispatch and subscription handling mechanisms.
This commit is contained in:
Harry
2025-08-30 19:36:24 +08:00
parent 3a66fc061b
commit e9df3cbd4a
11 changed files with 517 additions and 171 deletions
@@ -9,10 +9,10 @@ from typing import Any
import requests
from werkzeug import Request, Response
from dify_plugin.interfaces.trigger import TriggerProvider
from dify_plugin.entities.trigger import TriggerEventDispatch, Subscription, Unsubscription
from dify_plugin.entities.trigger import Subscription, TriggerDispatch, Unsubscription
from dify_plugin.errors.tool import ToolProviderCredentialValidationError, ToolProviderOAuthError
from dify_plugin.errors.trigger import SubscriptionError, WebhookValidationError, TriggerDispatchError
from dify_plugin.errors.trigger import SubscriptionError, TriggerDispatchError, WebhookValidationError
from dify_plugin.interfaces.trigger import TriggerProvider
class GithubProvider(TriggerProvider):
@@ -67,12 +67,12 @@ class GithubProvider(TriggerProvider):
except Exception as e:
raise ToolProviderCredentialValidationError(str(e)) from e
def _dispatch_event(self, subscription: Subscription, request: Request) -> TriggerEventDispatch:
def _dispatch_event(self, subscription: Subscription, request: Request) -> TriggerDispatch:
"""
Dispatch GitHub webhook events - focusing on issue comment events
"""
# Verify webhook signature if secret is provided
webhook_secret = subscription.configuration.get("webhook_secret")
webhook_secret = subscription.properties.get("webhook_secret")
if webhook_secret:
signature = request.headers.get("X-Hub-Signature-256")
if not signature:
@@ -102,25 +102,30 @@ class GithubProvider(TriggerProvider):
# Create trigger event dispatch with GitHub event type
# Map GitHub events to our trigger events
if event_type == "issue_comment":
return TriggerEventDispatch(event="issue_comment", response=response)
return TriggerDispatch(events=["issue_comment"], response=response)
elif event_type == "issues":
return TriggerEventDispatch(event="issues", response=response)
# Issues event can trigger multiple workflows based on action
action = payload.get("action")
if action == "opened":
# Dispatch both generic issues event and specific opened event
return TriggerDispatch(events=["issues", "issues.opened"], response=response)
elif action == "closed":
return TriggerDispatch(events=["issues", "issues.closed"], response=response)
else:
return TriggerDispatch(events=["issues"], response=response)
else:
# For other events, pass them through with prefix
return TriggerEventDispatch(event=f"github.{event_type}", response=response)
return TriggerDispatch(events=[f"github.{event_type}"], response=response)
def _subscribe(self, credentials: Mapping[str, Any], subscription_params: Mapping[str, Any]) -> Subscription:
def _subscribe(self, endpoint: str, credentials: Mapping[str, Any], parameters: Mapping[str, Any]) -> Subscription:
"""
Create a GitHub webhook subscription for issue comment events
"""
# Extract parameters
endpoint = subscription_params.get("endpoint")
webhook_secret = subscription_params.get("webhook_secret")
repository = subscription_params.get("repository") # format: "owner/repo"
events = subscription_params.get("events", ["issue_comment", "issues"])
webhook_secret = parameters.get("webhook_secret")
repository = parameters.get("repository") # format: "owner/repo"
events = parameters.get("events", ["issue_comment", "issues"])
if not endpoint:
raise ValueError("endpoint is required for webhook subscription")
if not repository:
raise ValueError("repository is required (format: owner/repo)")
@@ -154,9 +159,9 @@ class GithubProvider(TriggerProvider):
webhook = response.json()
# Return subscription with webhook details
return Subscription(
expire_at=int(time.time()) + 30 * 24 * 60 * 60, # 30 days expiration
expires_at=int(time.time()) + 30 * 24 * 60 * 60, # 30 days expiration
endpoint=endpoint,
configuration={
properties={
"external_id": str(webhook["id"]),
"webhook_url": webhook["url"],
"repository": repository,
@@ -175,17 +180,17 @@ class GithubProvider(TriggerProvider):
except requests.RequestException as e:
raise SubscriptionError(f"Network error while creating webhook: {e}", error_code="NETWORK_ERROR")
def _unsubscribe(self, subscription: Subscription, credentials: Mapping[str, Any]) -> Unsubscription:
def _unsubscribe(self, endpoint: str, subscription: Subscription, credentials: Mapping[str, Any]) -> Unsubscription:
"""
Remove a GitHub webhook subscription
"""
# Extract webhook details from data
external_id = subscription.configuration.get("external_id")
repository = subscription.configuration.get("repository")
# Extract webhook details from properties
external_id = subscription.properties.get("external_id")
repository = subscription.properties.get("repository")
if not external_id or not repository:
return Unsubscription(
success=False, message="Missing webhook ID or repository information", error_code="MISSING_DATA"
success=False, message="Missing webhook ID or repository information", error_code="MISSING_PROPERTIES"
)
# Parse repository
@@ -193,7 +198,7 @@ class GithubProvider(TriggerProvider):
owner, repo = repository.split("/")
except ValueError:
return Unsubscription(
success=False, message="Invalid repository format in data", error_code="INVALID_REPOSITORY"
success=False, message="Invalid repository format in properties", error_code="INVALID_REPOSITORY"
)
# Delete webhook using GitHub API
@@ -227,7 +232,7 @@ class GithubProvider(TriggerProvider):
success=False, message=f"Network error while deleting webhook: {e}", error_code="NETWORK_ERROR"
)
def _refresh(self, subscription: Subscription, credentials: Mapping[str, Any]) -> Subscription:
def _refresh(self, endpoint: str, subscription: Subscription, credentials: Mapping[str, Any]) -> Subscription:
"""
Refresh a GitHub webhook subscription (extend expiration)
GitHub webhooks don't expire, so we just extend our internal expiration
@@ -235,7 +240,7 @@ class GithubProvider(TriggerProvider):
# Simply return the subscription with extended expiration
# GitHub webhooks don't have built-in expiration
return Subscription(
expire_at=int(time.time()) + 30 * 24 * 60 * 60, # Extend by 30 days
endpoint=subscription.endpoint, # Keep the same endpoint
configuration=subscription.configuration, # Keep the same data
expires_at=int(time.time()) + 30 * 24 * 60 * 60, # Extend by 30 days
endpoint=endpoint, # Keep the same endpoint
properties=subscription.properties, # Keep the same properties
)
@@ -3,8 +3,8 @@ from typing import Any
from werkzeug import Request
from dify_plugin.entities.trigger import Event
from dify_plugin.interfaces.trigger import TriggerEvent
from dify_plugin.entities.trigger import TriggerEvent as TriggerEventResponse
class IssueCommentTrigger(TriggerEvent):
@@ -15,10 +15,10 @@ class IssueCommentTrigger(TriggerEvent):
from the webhook payload to provide as variables to the workflow.
"""
def _trigger(self, request: Request, values: Mapping[str, Any], parameters: Mapping[str, Any]) -> TriggerEventResponse:
def _trigger(self, request: Request, parameters: Mapping[str, Any]) -> Event:
"""
Handle GitHub issue comment event trigger
Parameters:
- action_filter: Filter by action type (created, edited, deleted, or any)
- issue_filter: Filter by specific issue number (optional)
@@ -30,19 +30,19 @@ class IssueCommentTrigger(TriggerEvent):
# Extract action type
action = payload.get("action", "")
# Apply action filter if specified
action_filter = parameters.get("action_filter", "any")
if action_filter != "any" and action != action_filter:
# Skip this event if it doesn't match the filter
return TriggerEventResponse(variables={})
# Extract issue comment information
comment = payload.get("comment", {})
issue = payload.get("issue", {})
repository = payload.get("repository", {})
sender = payload.get("sender", {})
# Apply issue number filter if specified
issue_filter = parameters.get("issue_filter")
if issue_filter is not None:
@@ -50,20 +50,20 @@ class IssueCommentTrigger(TriggerEvent):
if issue_number != int(issue_filter):
# Skip this event if it doesn't match the issue filter
return TriggerEventResponse(variables={})
# Check if this is a pull request
is_pull_request = "pull_request" in issue
# Extract labels
labels = [
{
"name": label.get("name", ""),
"color": label.get("color", ""),
"description": label.get("description", "")
"description": label.get("description", ""),
}
for label in issue.get("labels", [])
]
# Build variables for the workflow
variables = {
"action": action,
@@ -76,8 +76,8 @@ class IssueCommentTrigger(TriggerEvent):
"author": {
"login": comment.get("user", {}).get("login", ""),
"avatar_url": comment.get("user", {}).get("avatar_url", ""),
"html_url": comment.get("user", {}).get("html_url", "")
}
"html_url": comment.get("user", {}).get("html_url", ""),
},
},
"issue": {
"number": issue.get("number"),
@@ -94,10 +94,10 @@ class IssueCommentTrigger(TriggerEvent):
{
"login": assignee.get("login", ""),
"avatar_url": assignee.get("avatar_url", ""),
"html_url": assignee.get("html_url", "")
"html_url": assignee.get("html_url", ""),
}
for assignee in issue.get("assignees", [])
]
],
},
"repository": {
"name": repository.get("name", ""),
@@ -108,15 +108,15 @@ class IssueCommentTrigger(TriggerEvent):
"owner": {
"login": repository.get("owner", {}).get("login", ""),
"avatar_url": repository.get("owner", {}).get("avatar_url", ""),
"html_url": repository.get("owner", {}).get("html_url", "")
}
"html_url": repository.get("owner", {}).get("html_url", ""),
},
},
"sender": {
"login": sender.get("login", ""),
"avatar_url": sender.get("avatar_url", ""),
"html_url": sender.get("html_url", ""),
"type": sender.get("type", "")
}
"type": sender.get("type", ""),
},
}
return TriggerEventResponse(variables=variables)
return TriggerEventResponse(variables=variables)