feat(trigger): refactor trigger system with enhanced provider capabilities

- Update trigger provider interface with subscribe/unsubscribe actions
- Implement GitHub issue comment trigger example
- Enhance plugin executor to support trigger operations
- Improve request entity structure for trigger handling
- Remove deprecated push trigger implementation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Harry
2025-08-27 21:42:48 +08:00
parent ff61d5f204
commit aa417a750f
14 changed files with 904 additions and 221 deletions
+183 -11
View File
@@ -2,17 +2,19 @@ import hashlib
import hmac
import json
import secrets
import time
import urllib.parse
from collections.abc import Mapping
from typing import Any
import requests
from werkzeug import Request
from werkzeug import Request, Response
from dify_plugin.interfaces.trigger import TriggerProvider
from dify_plugin.entities.trigger import TriggerEventDispatch
from dify_plugin.entities.trigger import TriggerEventDispatch, Subscription, Unsubscription
from dify_plugin.entities.oauth import ToolOAuthCredentials
from dify_plugin.errors.tool import ToolProviderCredentialValidationError, ToolProviderOAuthError
from dify_plugin.errors.trigger import SubscriptionError, WebhookValidationError, TriggerDispatchError
class GithubProvider(TriggerProvider):
@@ -70,14 +72,14 @@ class GithubProvider(TriggerProvider):
def _dispatch_event(self, settings: Mapping[str, Any], request: Request) -> TriggerEventDispatch:
"""
Dispatch GitHub webhook events
Dispatch GitHub webhook events - focusing on issue comment events
"""
# Verify webhook signature if secret is provided
webhook_secret = settings.get("webhook_secret")
if webhook_secret:
signature = request.headers.get("X-Hub-Signature-256")
if not signature:
raise ValueError("Missing webhook signature")
raise WebhookValidationError("Missing webhook signature")
# Verify the signature
expected_signature = (
@@ -85,22 +87,192 @@ class GithubProvider(TriggerProvider):
)
if not hmac.compare_digest(signature, expected_signature):
raise ValueError("Invalid webhook signature")
raise WebhookValidationError("Invalid webhook signature")
event_type = request.headers.get("X-GitHub-Event")
if not event_type:
raise ValueError("Missing GitHub event type header")
raise TriggerDispatchError("Missing GitHub event type header")
try:
payload = request.get_json()
if not payload:
raise ValueError("Empty request body")
raise TriggerDispatchError("Empty request body")
except Exception as e:
raise ValueError(f"Failed to parse JSON payload: {e}")
from werkzeug import Response
raise TriggerDispatchError(f"Failed to parse JSON payload: {e}")
response = Response(response='{"status": "ok"}', status=200, mimetype="application/json")
# Create trigger event dispatch with GitHub event type
return TriggerEventDispatch(event=f"github.{event_type}", response=response)
# Map GitHub events to our trigger events
if event_type == "issue_comment":
return TriggerEventDispatch(event="issue_comment", response=response)
elif event_type == "issues":
return TriggerEventDispatch(event="issues", response=response)
else:
# For other events, pass them through with prefix
return TriggerEventDispatch(event=f"github.{event_type}", response=response)
def _subscribe(self, credentials: Mapping[str, Any], subscription_params: Mapping[str, Any]) -> Subscription:
"""
Create a GitHub webhook subscription for issue comment events
"""
# Extract parameters
callback_url = subscription_params.get("callback_url")
webhook_secret = subscription_params.get("webhook_secret")
repository = subscription_params.get("repository") # format: "owner/repo"
events = subscription_params.get("events", ["issue_comment", "issues"])
if not callback_url:
raise ValueError("callback_url is required for webhook subscription")
if not repository:
raise ValueError("repository is required (format: owner/repo)")
# Parse repository owner and name
try:
owner, repo = repository.split("/")
except ValueError:
raise ValueError("repository must be in format 'owner/repo'")
# Create webhook using GitHub API
url = f"https://api.github.com/repos/{owner}/{repo}/hooks"
headers = {
"Authorization": f"Bearer {credentials.get('access_tokens')}",
"Accept": "application/vnd.github+json",
}
webhook_data = {
"name": "web",
"active": True,
"events": events,
"config": {
"url": callback_url,
"content_type": "json",
"insecure_ssl": "0"
}
}
# Add secret if provided
if webhook_secret:
webhook_data["config"]["secret"] = webhook_secret
try:
response = requests.post(url, json=webhook_data, headers=headers, timeout=10)
if response.status_code == 201:
webhook = response.json()
# Return subscription with webhook details
return Subscription(
expire_at=int(time.time()) + 30 * 24 * 60 * 60, # 30 days expiration
metadata={
"external_id": str(webhook["id"]),
"webhook_url": webhook["url"],
"callback_url": callback_url,
"repository": repository,
"events": events,
"active": webhook["active"]
}
)
else:
error_msg = response.json().get("message", "Unknown error")
raise SubscriptionError(f"Failed to create GitHub webhook: {error_msg}",
error_code="WEBHOOK_CREATION_FAILED",
external_response=response.json())
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], settings: Mapping[str, Any]) -> Unsubscription:
"""
Remove a GitHub webhook subscription
"""
# Extract webhook details from metadata
external_id = subscription.metadata.get("external_id")
repository = subscription.metadata.get("repository")
if not external_id or not repository:
return Unsubscription(
success=False,
message="Missing webhook ID or repository information",
error_code="MISSING_METADATA"
)
# Parse repository
try:
owner, repo = repository.split("/")
except ValueError:
return Unsubscription(
success=False,
message="Invalid repository format in metadata",
error_code="INVALID_REPOSITORY"
)
# Delete webhook using GitHub API
url = f"https://api.github.com/repos/{owner}/{repo}/hooks/{external_id}"
headers = {
"Authorization": f"Bearer {credentials.get('access_tokens')}",
"Accept": "application/vnd.github+json",
}
try:
response = requests.delete(url, headers=headers, timeout=10)
if response.status_code == 204:
return Unsubscription(
success=True,
message=f"Successfully removed webhook {external_id} from {repository}"
)
elif response.status_code == 404:
return Unsubscription(
success=False,
message=f"Webhook {external_id} not found in repository {repository}",
error_code="WEBHOOK_NOT_FOUND"
)
else:
return Unsubscription(
success=False,
message=f"Failed to delete webhook: {response.json().get('message', 'Unknown error')}",
error_code="API_ERROR",
external_response=response.json()
)
except requests.RequestException as e:
return Unsubscription(
success=False,
message=f"Network error while deleting webhook: {e}",
error_code="NETWORK_ERROR"
)
def _refresh(self, 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
"""
# 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
metadata=subscription.metadata # Keep the same metadata
)
def _resubscribe(self, subscription: Subscription, credentials: Mapping[str, Any], settings: Mapping[str, Any]) -> Subscription:
"""
Update a GitHub webhook subscription with new settings
"""
# First unsubscribe the old webhook
unsubscribe_result = self._unsubscribe(subscription, credentials, settings)
if not unsubscribe_result.success:
# If we can't delete the old one, still try to create a new one
# This handles cases where the old webhook was already deleted
pass
# Extract new subscription parameters from settings
repository = subscription.metadata.get("repository")
callback_url = subscription.metadata.get("callback_url")
# Create new subscription params with updated settings
new_subscription_params = {
"callback_url": callback_url,
"repository": repository,
"webhook_secret": settings.get("webhook_secret"),
"events": settings.get("events", ["issue_comment", "issues"])
}
# Create new webhook with updated settings
return self._subscribe(credentials, new_subscription_params)
@@ -56,19 +56,60 @@ oauth_schema:
en_US: "Access Token"
subscription_schema:
- name: "repository"
type: "text-input"
required: true
label:
zh_Hans: "仓库"
en_US: "Repository"
placeholder:
zh_Hans: "owner/repo"
en_US: "owner/repo"
help:
en_US: "GitHub repository in format owner/repo (e.g., microsoft/vscode)"
pt_BR: "Repositório do GitHub no formato owner/repo (ex: microsoft/vscode)"
zh_Hans: "GitHub 仓库,格式为 owner/repo(例如:microsoft/vscode"
- name: "events"
type: "select"
required: true
default: ["issue_comment", "issues"]
options:
- value: "issue_comment"
label:
en_US: "Issue Comments"
zh_Hans: "Issue 评论"
- value: "issues"
label:
en_US: "Issues"
zh_Hans: "Issues"
- value: "pull_request"
label:
en_US: "Pull Requests"
zh_Hans: "Pull Requests"
- value: "push"
label:
en_US: "Push"
zh_Hans: "推送"
label:
zh_Hans: "监听事件"
en_US: "Events to Listen"
help:
en_US: "Select which GitHub events to subscribe to"
pt_BR: "Selecione quais eventos do GitHub inscrever-se"
zh_Hans: "选择要订阅的 GitHub 事件"
- name: "webhook_secret"
type: "secret-input"
required: true
required: false
label:
zh_Hans: "Webhook Secret"
en_US: "Webhook Secret"
help:
en_US: "Webhook secret for validating GitHub webhook requests"
pt_BR: "Segredo do webhook para validar solicitações do webhook do GitHub"
zh_Hans: "用于验证 GitHub webhook 请求的 webhook 密钥"
en_US: "Optional webhook secret for validating GitHub webhook requests"
pt_BR: "Segredo opcional do webhook para validar solicitações do webhook do GitHub"
zh_Hans: "可选的用于验证 GitHub webhook 请求的 webhook 密钥"
triggers:
- triggers/push_trigger.yaml
- triggers/issue_comment.yaml
identity:
author: langgenius
@@ -1 +0,0 @@
# GitHub Triggers
@@ -0,0 +1,122 @@
from collections.abc import Mapping
from typing import Any
from werkzeug import Request
from dify_plugin.interfaces.trigger import TriggerEvent
from dify_plugin.entities.trigger import TriggerEvent as TriggerEventResponse
class IssueCommentTrigger(TriggerEvent):
"""
GitHub Issue Comment Event Trigger
This trigger handles GitHub issue comment events and extracts relevant information
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:
"""
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)
"""
# Get the event payload
payload = request.get_json()
if not payload:
raise ValueError("No payload received")
# 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:
issue_number = issue.get("number")
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", "")
}
for label in issue.get("labels", [])
]
# Build variables for the workflow
variables = {
"action": action,
"comment": {
"id": comment.get("id"),
"body": comment.get("body", ""),
"html_url": comment.get("html_url", ""),
"created_at": comment.get("created_at", ""),
"updated_at": comment.get("updated_at", ""),
"author": {
"login": comment.get("user", {}).get("login", ""),
"avatar_url": comment.get("user", {}).get("avatar_url", ""),
"html_url": comment.get("user", {}).get("html_url", "")
}
},
"issue": {
"number": issue.get("number"),
"title": issue.get("title", ""),
"state": issue.get("state", ""),
"html_url": issue.get("html_url", ""),
"body": issue.get("body", ""),
"labels": labels,
"is_pull_request": is_pull_request,
"created_at": issue.get("created_at", ""),
"updated_at": issue.get("updated_at", ""),
"closed_at": issue.get("closed_at"),
"assignees": [
{
"login": assignee.get("login", ""),
"avatar_url": assignee.get("avatar_url", ""),
"html_url": assignee.get("html_url", "")
}
for assignee in issue.get("assignees", [])
]
},
"repository": {
"name": repository.get("name", ""),
"full_name": repository.get("full_name", ""),
"html_url": repository.get("html_url", ""),
"description": repository.get("description", ""),
"private": repository.get("private", False),
"owner": {
"login": repository.get("owner", {}).get("login", ""),
"avatar_url": repository.get("owner", {}).get("avatar_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", "")
}
}
return TriggerEventResponse(variables=variables)
@@ -0,0 +1,136 @@
identity:
name: issue_comment
author: langgenius
label:
en_US: Issue Comment Event
zh_Hans: Issue 评论事件
pt_BR: Evento de Comentário de Issue
description:
human:
en_US: Triggers when someone comments on an issue or pull request
zh_Hans: 当有人在 issue 或 pull request 上评论时触发
pt_BR: Dispara quando alguém comenta em uma issue ou pull request
llm:
en_US: This trigger activates when a comment is created, edited, or deleted on a GitHub issue or pull request, providing information about the comment, issue, and user.
zh_Hans: 当在 GitHub issue 或 pull request 上创建、编辑或删除评论时,此触发器会被激活,提供有关评论、issue 和用户的信息。
pt_BR: Este gatilho é ativado quando um comentário é criado, editado ou deletado em uma issue ou pull request do GitHub, fornecendo informações sobre o comentário, issue e usuário.
parameters:
- name: action_filter
label:
en_US: Action Filter
zh_Hans: 动作过滤器
pt_BR: Filtro de Ação
type: select
required: false
default: "any"
options:
- value: "any"
label:
en_US: Any Action
zh_Hans: 任何动作
- value: "created"
label:
en_US: Created
zh_Hans: 创建
- value: "edited"
label:
en_US: Edited
zh_Hans: 编辑
- value: "deleted"
label:
en_US: Deleted
zh_Hans: 删除
description:
en_US: Filter by comment action type
zh_Hans: 按评论动作类型过滤
pt_BR: Filtrar por tipo de ação do comentário
- name: issue_filter
label:
en_US: Issue Number Filter
zh_Hans: Issue 编号过滤器
pt_BR: Filtro de Número da Issue
type: number
required: false
description:
en_US: Filter by specific issue number (optional)
zh_Hans: 按特定 issue 编号过滤(可选)
pt_BR: Filtrar por número específico da issue (opcional)
output_schema:
type: object
properties:
action:
type: string
description: The action performed on the comment (created, edited, deleted)
comment:
type: object
properties:
id:
type: number
description: The ID of the comment
body:
type: string
description: The content of the comment
html_url:
type: string
description: The URL to view the comment on GitHub
created_at:
type: string
description: When the comment was created
updated_at:
type: string
description: When the comment was last updated
issue:
type: object
properties:
number:
type: number
description: The issue number
title:
type: string
description: The issue title
state:
type: string
description: The issue state (open, closed)
html_url:
type: string
description: The URL to view the issue on GitHub
body:
type: string
description: The issue description
labels:
type: array
description: Labels attached to the issue
is_pull_request:
type: boolean
description: Whether this is a pull request
repository:
type: object
properties:
name:
type: string
description: The repository name
full_name:
type: string
description: The full repository name (owner/repo)
html_url:
type: string
description: The repository URL
sender:
type: object
properties:
login:
type: string
description: The username of the comment author
avatar_url:
type: string
description: The avatar URL of the comment author
html_url:
type: string
description: The profile URL of the comment author
extra:
python:
source: triggers/issue_comment.py
@@ -1,80 +0,0 @@
from collections.abc import Mapping
from typing import Any
from werkzeug import Request
from dify_plugin.interfaces.trigger import Trigger
from dify_plugin.entities.trigger import TriggerEvent
class PushTrigger(Trigger):
"""
GitHub Push Event Trigger
This trigger handles GitHub push events and extracts relevant information
from the webhook payload to provide as variables to the workflow.
"""
def _trigger(self, request: Request, values: Mapping[str, Any], parameters: Mapping[str, Any]) -> TriggerEvent:
"""
Handle GitHub push event trigger
"""
# Get the event payload
payload = request.get_json()
if not payload:
raise ValueError("No payload received")
# Extract push event information
repository = payload.get("repository", {})
pusher = payload.get("pusher", {})
commits = payload.get("commits", [])
ref = payload.get("ref", "")
# Extract branch name from ref (refs/heads/main -> main)
branch = ref.split("/")[-1] if ref.startswith("refs/heads/") else ref
# Build variables for the workflow
variables = {
"repository_name": repository.get("name", ""),
"repository_full_name": repository.get("full_name", ""),
"repository_url": repository.get("html_url", ""),
"branch": branch,
"ref": ref,
"pusher_name": pusher.get("name", ""),
"pusher_email": pusher.get("email", ""),
"commits_count": len(commits),
"commits": [
{
"id": commit.get("id", ""),
"message": commit.get("message", ""),
"author": {
"name": commit.get("author", {}).get("name", ""),
"email": commit.get("author", {}).get("email", ""),
},
"url": commit.get("url", ""),
"added": commit.get("added", []),
"removed": commit.get("removed", []),
"modified": commit.get("modified", []),
}
for commit in commits
],
"head_commit": None,
}
# Add head commit information if available
head_commit = payload.get("head_commit")
if head_commit:
variables["head_commit"] = {
"id": head_commit.get("id", ""),
"message": head_commit.get("message", ""),
"author": {
"name": head_commit.get("author", {}).get("name", ""),
"email": head_commit.get("author", {}).get("email", ""),
},
"url": head_commit.get("url", ""),
"added": head_commit.get("added", []),
"removed": head_commit.get("removed", []),
"modified": head_commit.get("modified", []),
}
return TriggerEvent(variables=variables)
@@ -1,82 +0,0 @@
identity:
name: push_trigger
author: langgenius
label:
en_US: Push Event
zh_Hans: 推送事件
pt_BR: Evento de Push
description:
human:
en_US: Triggers when someone pushes code to a repository
zh_Hans: 当有人向仓库推送代码时触发
pt_BR: Dispara quando alguém envia código para um repositório
llm:
en_US: This trigger activates when a push event occurs in a GitHub repository, providing information about the commits, repository, and pusher.
zh_Hans: 当GitHub仓库中发生推送事件时,此触发器会被激活,提供有关提交、仓库和推送者的信息。
pt_BR: Este gatilho é ativado quando um evento de push ocorre em um repositório GitHub, fornecendo informações sobre os commits, repositório e quem fez o push.
parameters:
- name: repository_filter
label:
en_US: Repository Filter
zh_Hans: 仓库过滤器
pt_BR: Filtro de Repositório
type: string
required: false
description:
en_US: Filter by repository name (optional)
zh_Hans: 按仓库名称过滤(可选)
pt_BR: Filtrar por nome do repositório (opcional)
- name: branch_filter
label:
en_US: Branch Filter
zh_Hans: 分支过滤器
pt_BR: Filtro de Branch
type: string
required: false
description:
en_US: Filter by branch name (optional)
zh_Hans: 按分支名称过滤(可选)
pt_BR: Filtrar por nome da branch (opcional)
output_schema:
type: object
properties:
push_event:
type: object
properties:
repository_name:
type: string
description: The name of the repository
repository_full_name:
type: string
description: The full name of the repository (owner/repo)
repository_url:
type: string
description: The URL of the repository
branch:
type: string
description: The branch name that was pushed to
ref:
type: string
description: The git reference that was pushed (e.g., refs/heads/main)
pusher_name:
type: string
description: The name of the user who pushed
pusher_email:
type: string
description: The email of the user who pushed
commits_count:
type: number
description: The number of commits in this push
commits:
type: array
description: List of commits in this push
head_commit:
type: object
description: Information about the head commit
extra:
python:
source: triggers/push_trigger.py