From 40dc44873c18d4bd4fdd77d42cd8048bb43146b5 Mon Sep 17 00:00:00 2001 From: Gu Date: Thu, 20 Nov 2025 11:05:25 +0800 Subject: [PATCH] incorporate plugin file changes --- plugin-dev-en/0131-cheatsheet.mdx | 166 ++++++ plugin-dev-en/0222-trigger-plugin.mdx | 664 ++++++++++++++++++++++ plugin-dev-en/9241-bundle.mdx | 2 +- plugin-dev-ja/0131-cheatsheet.mdx | 4 + plugin-dev-ja/0222-trigger-plugin.mdx | 666 +++++++++++++++++++++++ plugin-dev-ja/9241-bundle.mdx | 2 +- plugin-dev-zh/0131-cheatsheet.mdx | 12 +- plugin-dev-zh/0222-datasource-plugin.mdx | 2 +- plugin-dev-zh/0222-trigger-plugin.mdx | 660 ++++++++++++++++++++++ plugin-dev-zh/9231-extension-plugin.mdx | 2 + plugin-dev-zh/9241-bundle.mdx | 2 +- 11 files changed, 2174 insertions(+), 8 deletions(-) create mode 100644 plugin-dev-en/0131-cheatsheet.mdx create mode 100644 plugin-dev-en/0222-trigger-plugin.mdx create mode 100644 plugin-dev-ja/0222-trigger-plugin.mdx create mode 100644 plugin-dev-zh/0222-trigger-plugin.mdx diff --git a/plugin-dev-en/0131-cheatsheet.mdx b/plugin-dev-en/0131-cheatsheet.mdx new file mode 100644 index 00000000..487746e5 --- /dev/null +++ b/plugin-dev-en/0131-cheatsheet.mdx @@ -0,0 +1,166 @@ +--- +dimensions: + type: + primary: conceptual + detail: architecture + level: beginner +standard_title: Cheatsheet +language: en +title: Dify Plugin Development Cheatsheet +description: A comprehensive reference guide for Dify plugin development, including + environment requirements, installation methods, development process, plugin categories + and types, common code snippets, and solutions to common issues. Suitable for developers + to quickly consult and reference. +--- + +### Environment Requirements + +- Python version ≥ 3.12 +- Dify plugin scaffold tool (dify-plugin-daemon) + +> Learn more: [Initializing Development Tools](/plugin-dev-en/0221-initialize-development-tools) + +### Obtaining the Dify Plugin Development Package + +[Dify Plugin CLI](https://github.com/langgenius/dify-plugin-daemon/releases) + +#### Installation Methods for Different Platforms + +**macOS [Brew](https://github.com/langgenius/homebrew-dify) (Global Installation):** + +```bash +brew tap langgenius/dify +brew install dify +``` + +After installation, open a new terminal window and enter the `dify version` command. If it outputs the version information, the installation was successful. + +**macOS ARM (M Series Chips):** + +```bash +# Download dify-plugin-darwin-arm64 +chmod +x dify-plugin-darwin-arm64 +./dify-plugin-darwin-arm64 version +``` + +**macOS Intel:** + +```bash +# Download dify-plugin-darwin-amd64 +chmod +x dify-plugin-darwin-amd64 +./dify-plugin-darwin-amd64 version +``` + +**Linux:** + +```bash +# Download dify-plugin-linux-amd64 +chmod +x dify-plugin-linux-amd64 +./dify-plugin-linux-amd64 version +``` + +**Global Installation (Recommended):** + +```bash +# Rename and move to system path +# Example (macOS ARM) +mv dify-plugin-darwin-arm64 dify +sudo mv dify /usr/local/bin/ +dify version +``` + +### Running the Development Package + +Here we use `dify` as an example. If you are using a local installation method, please replace the command accordingly, for example `./dify-plugin-darwin-arm64 plugin init`. + +### Plugin Development Process + +#### 1. Create a New Plugin + +```bash +./dify plugin init +``` + +Follow the prompts to complete the basic plugin information configuration + +> Learn more: [Dify Plugin Development: Hello World Guide](/plugin-dev-en/0211-getting-started-dify-tool) + +#### 2. Run in Development Mode + +Configure the `.env` file, then run the following command in the plugin directory: + +```bash +python -m main +``` + +> Learn more: [Remote Debugging Plugins](/plugin-dev-en/0411-remote-debug-a-plugin) + +#### 4. Packaging and Deployment + +Package the plugin: + +```bash +cd .. +dify plugin package ./yourapp +``` + +> Learn more: [Publishing Overview](/plugin-dev-en/0321-release-overview) + +### Plugin Categories + +#### Tool Labels + +Category `tag` [class ToolLabelEnum(Enum)](https://github.com/langgenius/dify-plugin-sdks/blob/main/python/dify_plugin/entities/tool.py) + +```python +class ToolLabelEnum(Enum): + SEARCH = "search" + IMAGE = "image" + VIDEOS = "videos" + WEATHER = "weather" + FINANCE = "finance" + DESIGN = "design" + TRAVEL = "travel" + SOCIAL = "social" + NEWS = "news" + MEDICAL = "medical" + PRODUCTIVITY = "productivity" + EDUCATION = "education" + BUSINESS = "business" + ENTERTAINMENT = "entertainment" + UTILITIES = "utilities" + OTHER = "other" +``` + +### Plugin Type Reference + +Dify supports the development of various types of plugins: + +- **Tool plugin**: Integrate third-party APIs and services + > Learn more: [Dify Plugin Development: Hello World Guide](/plugin-dev-en/0211-getting-started-dify-tool) + +- **Model plugin**: Integrate AI models + > Learn more: [Model Plugin](/plugin-dev-en/0411-model-plugin-introduction), [Quick Integration of a New Model](/plugin-dev-en/0211-getting-started-new-model) + +- **Agent strategy plugin**: Customize Agent thinking and decision-making strategies + > Learn more: [Agent Strategy Plugin](/plugin-dev-en/9433-agent-strategy-plugin) + +- **Extension plugin**: Extend Dify platform functionality, such as Endpoints and WebAPP + > Learn more: [Extension Plugin](/plugin-dev-en/9231-extension-plugin) + +- **Data source plugin**: Serve as the document data source and starting point for knowledge pipelines + > Learn more: [Data Source Plugin](/plugin-dev-en/0222-datasource-plugin) + +- **Trigger plugin**: Automatically trigger Workflow execution upon third-party events + > Learn more: [Trigger Plugin](/plugin-dev-en/0222-trigger-plugin) + +{/* +Contributing Section +DO NOT edit this section! +It will be automatically generated by the script. +*/} + +--- + +[Edit this page](https://github.com/langgenius/dify-docs/edit/main/plugin-dev-en/0131-cheatsheet.mdx) | [Report an issue](https://github.com/langgenius/dify-docs/issues/new?template=docs.yml) + diff --git a/plugin-dev-en/0222-trigger-plugin.mdx b/plugin-dev-en/0222-trigger-plugin.mdx new file mode 100644 index 00000000..d5717bd5 --- /dev/null +++ b/plugin-dev-en/0222-trigger-plugin.mdx @@ -0,0 +1,664 @@ +--- +title: "Trigger Plugin" +--- + +## What Is a Trigger Plugin? + +Triggers were introduced in Dify v1.10.0 as a new type of start node. Unlike functional nodes such as Code, Tool, or Knowledge Retrieval, the purpose of a trigger is to **convert third-party events into an input format that Dify can recognize and process**. + +![Trigger Plugin Intro](/images/trigger_plugin_intro.PNG) + +For example, if you configure Dify as the `new email` event receiver in Gmail, every time you receive a new email, Gmail automatically sends an event to Dify that can be used to trigger a workflow. However: + +- Gmail's original event format is not compatible with Dify's input format. + +- There are thousands of platforms worldwide, each with its own unique event format. + +Therefore, we need trigger plugins to define and parse these events from different platforms and in various formats, and to unify them into an input format that Dify can accept. + +## Technical Overview + +Dify triggers are implemented based on webhooks, a widely adopted mechanism across the web. Many mainstream SaaS platforms (like GitHub, Slack, and Linear) support webhooks with comprehensive developer documentation. + +A webhook can be understood as an HTTP-based event dispatcher. **Once an event-receiving address is configured, these SaaS platforms automatically push event data to the target server whenever a subscribed event occurs.** + +To handle webhook events from different platforms in a unified way, Dify defines two core concepts: **Subscription** and **Event**. + +- **Subscription**: Webhook-based event dispatch requires **registering Dify's network address on a third-party platform's developer console as the target server. In Dify, this configuration process is called a *Subscription*.** + +- **Event**: A platform may send multiple types of events—such as *email received*, *email deleted*, or *email marked as read*—all of which are pushed to the registered address. A trigger plugin can handle multiple event types, with each event corresponding to a plugin trigger node in a Dify workflow. + +## Plugin Development + +The development process for a trigger plugin is consistent with that of other plugin types (Tool, Data Source, Model, etc.). + +You can create a development template using the `dify plugin init` command. The generated file structure follows the standard plugin format specification. + +``` +├── _assets +│   └── icon.svg +├── events +│   └── star +│   ├── star_created.py +│   └── star_created.yaml +├── main.py +├── manifest.yaml +├── provider +│   ├── github.py +│   └── github.yaml +├── README.md +├── PRIVACY.md +└── requirements.txt +``` + +- `manifest.yaml`: Describes the plugin's basic metadata. + +- `provider` directory: Contains the provider's metadata, the code for creating subscriptions, and the code for classifying events after receiving webhook requests. + +- **`events` directory: Contains the code for event handling and filtering, which supports local event filtering at the node level. You can create subdirectories to group related events.** + + + For trigger plugins, the minimum required Dify version must be set to `1.10.0`, and the SDK version must be `>= 0.6.0`. + + +Next, we'll use GitHub as an example to illustrate the development process of a trigger plugin. + +### Subscription Creation + +Webhook configuration methods vary significantly across mainstream SaaS platforms: + +- Some platforms (such as GitHub) support API-based webhook configuration. For these platforms, once OAuth authentication is completed, Dify can automatically set up the webhook. + +- Other platforms (such as Notion) do not provide a webhook configuration API and may require users to perform manual authentication. + +To accommodate these differences, we divide the subscription process into two parts: the **Subscription Constructor** and the **Subscription** itself. + +For platforms like Notion, creating a subscription requires the user to manually copy the callback URL provided by Dify and paste it into their Notion workspace to complete the webhook setup. This process corresponds to the **Paste URL to create a new subscription** option in the Dify interface. + +Paste URL to Create a New Subscription + +To implement subscription creation via manual URL pasting, you need to modify two files: `github.yaml` and `github.py`. + + + + Since GitHub webhooks use an encryption mechanism, a secret key is required to decrypt and validate incoming requests. Therefore, you need to declare `webhook_secret` in `github.yaml`. + + ```YAML + subscription_schema: + - name: "webhook_secret" + type: "secret-input" + required: false + label: + zh_Hans: "Webhook Secret" + en_US: "Webhook Secret" + ja_JP: "Webhookシークレット" + help: + en_US: "Optional webhook secret for validating GitHub webhook requests" + ja_JP: "GitHub Webhookリクエストの検証用のオプションのWebhookシークレット" + zh_Hans: "可选的用于验证 GitHub webhook 请求的 webhook 密钥" + ``` + + + + First, we need to implement the `dispatch_event` interface. All requests sent to the callback URL are processed by this interface, and the processed events will be displayed in the **Request Logs** section for debugging and verification. + + Manual Setup + + In the code, you can retrieve the `webhook_secret` declared in `github.yaml` via `subscription.properties`. + + The `dispatch_event` method needs to determine the event type based on the request content. In the example below, this event extraction is handled by the `_dispatch_trigger_event` method. + + + For the complete code sample, see [Dify's GitHub trigger plugin](https://github.com/langgenius/dify-plugin-sdks/tree/feat/trigger/python/examples/github_trigger). + + + ```Python + class GithubTrigger(Trigger): + """Handle GitHub webhook event dispatch.""" + + def _dispatch_event(self, subscription: Subscription, request: Request) -> EventDispatch: + webhook_secret = subscription.properties.get("webhook_secret") + if webhook_secret: + self._validate_signature(request=request, webhook_secret=webhook_secret) + + event_type: str | None = request.headers.get("X-GitHub-Event") + if not event_type: + raise TriggerDispatchError("Missing GitHub event type header") + + payload: Mapping[str, Any] = self._validate_payload(request) + response = Response(response='{"status": "ok"}', status=200, mimetype="application/json") + event: str = self._dispatch_trigger_event(event_type=event_type, payload=payload) + return EventDispatch(events=[event] if event else [], response=response) + ``` + + + +### Event Handling + +Once an event is extracted, the corresponding implementation must filter the original HTTP request and transform it into an input format that Dify workflows can accept. + +Taking the Issue event as an example, you can define the event and its implementation through `events/issues/issues.yaml` and `events/issues/issues.py`, respectively. The event's output can be defined in the `output_schema` section of `issues.yaml`, which follows the same JSON Schema specification as tool plugins. + + + + ```YAML + identity: + name: issues + author: langgenius + label: + en_US: Issues + zh_Hans: 议题 + ja_JP: イシュー + description: + en_US: Unified issues event with actions filter + zh_Hans: 带 actions 过滤的统一 issues 事件 + ja_JP: アクションフィルタ付きの統合イシューイベント + output_schema: + type: object + properties: + action: + type: string + issue: + type: object + description: The issue itself + extra: + python: + source: events/issues/issues.py + ``` + + + ```Python + from collections.abc import Mapping + from typing import Any + + from werkzeug import Request + + from dify_plugin.entities.trigger import Variables + from dify_plugin.errors.trigger import EventIgnoreError + from dify_plugin.interfaces.trigger import Event + + class IssuesUnifiedEvent(Event): + """Unified Issues event. Filters by actions and common issue attributes.""" + + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + payload = request.get_json() + if not payload: + raise ValueError("No payload received") + + allowed_actions = parameters.get("actions") or [] + action = payload.get("action") + if allowed_actions and action not in allowed_actions: + raise EventIgnoreError() + + issue = payload.get("issue") + if not isinstance(issue, Mapping): + raise ValueError("No issue in payload") + + return Variables(variables={**payload}) + ``` + + + +### Event Filtering + +To filter out certain events—for example, to focus only on Issue events with a specific label—you can add `parameters` to the event definition in `issues.yaml`. Then, in the `_on_event` method, you can throw an `EventIgnoreError` exception to filter out events that do not meet the configured criteria. + + + + ```YAML + parameters: + - name: added_label + label: + en_US: Added Label + zh_Hans: 添加的标签 + ja_JP: 追加されたラベル + type: string + required: false + description: + en_US: "Only trigger if these specific labels were added (e.g., critical, priority-high, security, comma-separated). Leave empty to trigger for any label addition." + zh_Hans: "仅当添加了这些特定标签时触发(例如:critical, priority-high, security,逗号分隔)。留空则对任何标签添加触发。" + ja_JP: "これらの特定のラベルが追加された場合のみトリガー(例: critical, priority-high, security,カンマ区切り)。空の場合は任意のラベル追加でトリガー。" + ``` + + + ```Python + def _check_added_label(self, payload: Mapping[str, Any], added_label_param: str | None) -> None: + """Check if the added label matches the allowed labels""" + if not added_label_param: + return + + allowed_labels = [label.strip() for label in added_label_param.split(",") if label.strip()] + if not allowed_labels: + return + + # The payload contains the label that was added + label = payload.get("label", {}) + label_name = label.get("name", "") + + if label_name not in allowed_labels: + raise EventIgnoreError() + + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + # ... + # Apply all filters + self._check_added_label(payload, parameters.get("added_label")) + + return Variables(variables={**payload}) + ``` + + + +### Subscription Creation via OAuth or API Key + +To enable automatic subscription creation via OAuth or API key, you need to modify the `github.yaml` and `github.py` files. + + + + In `github.yaml`, add the following fields. + + ```YAML + subscription_constructor: + parameters: + - name: "repository" + label: + en_US: "Repository" + zh_Hans: "仓库" + ja_JP: "リポジトリ" + type: "dynamic-select" + required: true + placeholder: + en_US: "owner/repo" + zh_Hans: "owner/repo" + ja_JP: "owner/repo" + help: + en_US: "GitHub repository in format owner/repo (e.g., microsoft/vscode)" + zh_Hans: "GitHub 仓库,格式为 owner/repo(例如:microsoft/vscode)" + ja_JP: "GitHubリポジトリは owner/repo 形式で入力してください(例: microsoft/vscode)" + credentials_schema: + access_tokens: + help: + en_US: Get your Access Tokens from GitHub + ja_JP: GitHub からアクセストークンを取得してください + zh_Hans: 从 GitHub 获取您的 Access Tokens + label: + en_US: Access Tokens + ja_JP: アクセストークン + zh_Hans: Access Tokens + placeholder: + en_US: Please input your GitHub Access Tokens + ja_JP: GitHub のアクセストークンを入力してください + zh_Hans: 请输入你的 GitHub Access Tokens + required: true + type: secret-input + url: https://github.com/settings/tokens?type=beta + extra: + python: + source: provider/github.py + ``` + + `subscription_constructor` is a concept abstracted by Dify to define how a subscription is constructed. It includes the following fields: + + - `parameters` (optional): Defines the parameters required to create a subscription, such as the event types to subscribe to or the target GitHub repository + + - `credentials_schema` (optional): Declares the required credentials for creating a subscription with an API key or access token, such as `access_tokens` for GitHub. + + - `oauth_schema` (optional): Required for implementing subscription creation via OAuth. For details on how to define it, see [Add OAuth Support to Your Tool Plugin](/plugin-dev-en/0222-tool-oauth). + + + In `github.py`, create a `Constructor` class to implement the automatic subscription logic. + + ```Python + class GithubSubscriptionConstructor(TriggerSubscriptionConstructor): + """Manage GitHub trigger subscriptions.""" + def _validate_api_key(self, credentials: Mapping[str, Any]) -> None: + # ... + + def _create_subscription( + self, + endpoint: str, + parameters: Mapping[str, Any], + credentials: Mapping[str, Any], + credential_type: CredentialType, + ) -> Subscription: + repository = parameters.get("repository") + if not repository: + raise ValueError("repository is required (format: owner/repo)") + + try: + owner, repo = repository.split("/") + except ValueError: + raise ValueError("repository must be in format 'owner/repo'") from None + + events: list[str] = parameters.get("events", []) + webhook_secret = uuid.uuid4().hex + 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": endpoint, "content_type": "json", "insecure_ssl": "0", "secret": webhook_secret}, + } + + try: + response = requests.post(url, json=webhook_data, headers=headers, timeout=10) + except requests.RequestException as exc: + raise SubscriptionError(f"Network error while creating webhook: {exc}", error_code="NETWORK_ERROR") from exc + + if response.status_code == 201: + webhook = response.json() + return Subscription( + expires_at=int(time.time()) + self._WEBHOOK_TTL, + endpoint=endpoint, + parameters=parameters, + properties={ + "external_id": str(webhook["id"]), + "repository": repository, + "events": events, + "webhook_secret": webhook_secret, + "active": webhook.get("active", True), + }, + ) + + response_data: dict[str, Any] = response.json() if response.content else {} + error_msg = response_data.get("message", "Unknown error") + error_details = response_data.get("errors", []) + detailed_error = f"Failed to create GitHub webhook: {error_msg}" + if error_details: + detailed_error += f" Details: {error_details}" + + raise SubscriptionError( + detailed_error, + error_code="WEBHOOK_CREATION_FAILED", + external_response=response_data, + ) + ``` + + + +--- + +Once you have modified these two files, you'll see the **Create with API Key** option in the Dify interface. + +Automatic subscription creation via OAuth can also be implemented in the same `Constructor` class: by adding an `oauth_schema` field under `subscription_constructor`, you can enable OAuth authentication. + +OAuth & API Key Options + +## Explore More + +The interface definitions and implementation methods of core classes in trigger plugin development are as follows. + +### Trigger + +```Python +class Trigger(ABC): + @abstractmethod + def _dispatch_event(self, subscription: Subscription, request: Request) -> EventDispatch: + """ + Internal method to implement event dispatch logic. + + Subclasses must override this method to handle incoming webhook events. + + Implementation checklist: + 1. Validate the webhook request: + - Check signature/HMAC using properties when you create the subscription from subscription.properties + - Verify request is from expected source + 2. Extract event information: + - Parse event type from headers or body + - Extract relevant payload data + 3. Return EventDispatch with: + - events: List of Event names to invoke (can be single or multiple) + - response: Appropriate HTTP response for the webhook + + Args: + subscription: The Subscription object with endpoint and properties fields + request: Incoming webhook HTTP request + + Returns: + EventDispatch: Event dispatch routing information + + Raises: + TriggerValidationError: For security validation failures + TriggerDispatchError: For parsing or routing errors + """ + raise NotImplementedError("This plugin should implement `_dispatch_event` method to enable event dispatch") + +``` + +### TriggerSubscriptionConstructor + +```Python +class TriggerSubscriptionConstructor(ABC, OAuthProviderProtocol): + # OPTIONAL + def _validate_api_key(self, credentials: Mapping[str, Any]) -> None: + raise NotImplementedError( + "This plugin should implement `_validate_api_key` method to enable credentials validation" + ) + + # OPTIONAL + def _oauth_get_authorization_url(self, redirect_uri: str, system_credentials: Mapping[str, Any]) -> str: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_get_authorization_url` method" + ) + + # OPTIONAL + def _oauth_get_credentials( + self, redirect_uri: str, system_credentials: Mapping[str, Any], request: Request + ) -> TriggerOAuthCredentials: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_get_credentials` method" + ) + + # OPTIONAL + def _oauth_refresh_credentials( + self, redirect_uri: str, system_credentials: Mapping[str, Any], credentials: Mapping[str, Any] + ) -> OAuthCredentials: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_refresh_credentials` method" + ) + + @abstractmethod + def _create_subscription( + self, + endpoint: str, + parameters: Mapping[str, Any], + credentials: Mapping[str, Any], + credential_type: CredentialType, + ) -> Subscription: + """ + Internal method to implement subscription logic. + + Subclasses must override this method to handle subscription creation. + + Implementation checklist: + 1. Use the endpoint parameter provided by Dify + 2. Register webhook with external service using their API + 3. Store all necessary information in Subscription.properties for future operations(e.g., dispatch_event) + 4. Return Subscription with: + - expires_at: Set appropriate expiration time + - endpoint: The webhook endpoint URL allocated by Dify for receiving events, same with the endpoint parameter + - parameters: The parameters of the subscription + - properties: All configuration and external IDs + + Args: + endpoint: The webhook endpoint URL allocated by Dify for receiving events + parameters: Subscription creation parameters + credentials: Authentication credentials + credential_type: The type of the credentials, e.g., "api-key", "oauth2", "unauthorized" + + Returns: + Subscription: Subscription details with metadata for future operations + + Raises: + SubscriptionError: For operational failures (API errors, invalid credentials) + ValueError: For programming errors (missing required params) + """ + raise NotImplementedError( + "This plugin should implement `_create_subscription` method to enable event subscription" + ) + + @abstractmethod + def _delete_subscription( + self, subscription: Subscription, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> UnsubscribeResult: + """ + Internal method to implement unsubscription logic. + + Subclasses must override this method to handle subscription removal. + + Implementation guidelines: + 1. Extract necessary IDs from subscription.properties (e.g., external_id) + 2. Use credentials and credential_type to call external service API to delete the webhook + 3. Handle common errors (not found, unauthorized, etc.) + 4. Always return UnsubscribeResult with detailed status + 5. Never raise exceptions for operational failures - use UnsubscribeResult.success=False + + Args: + subscription: The Subscription object with endpoint and properties fields + + Returns: + UnsubscribeResult: Always returns result, never raises for operational failures + """ + raise NotImplementedError( + "This plugin should implement `_delete_subscription` method to enable event unsubscription" + ) + + @abstractmethod + def _refresh_subscription( + self, subscription: Subscription, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> Subscription: + """ + Internal method to implement subscription refresh logic. + + Subclasses must override this method to handle simple expiration extension. + + Implementation patterns: + 1. For webhooks without expiration (e.g., GitHub): + - Update the Subscription.expires_at=-1 then Dify will never call this method again + + 2. For lease-based subscriptions (e.g., Microsoft Graph): + - Use the information in Subscription.properties to call service's lease renewal API if available + - Handle renewal limits (some services limit renewal count) + - Update the Subscription.properties and Subscription.expires_at for next time renewal if needed + + Args: + subscription: Current subscription with properties + credential_type: The type of the credentials, e.g., "api-key", "oauth2", "unauthorized" + credentials: Current authentication credentials from credentials_schema. + For API key auth, according to `credentials_schema` defined in the YAML. + For OAuth auth, according to `oauth_schema.credentials_schema` defined in the YAML. + For unauthorized auth, there is no credentials. + + Returns: + Subscription: Same subscription with extended expiration + or new properties and expires_at for next time renewal + + Raises: + SubscriptionError: For operational failures (API errors, invalid credentials) + """ + raise NotImplementedError("This plugin should implement `_refresh` method to enable subscription refresh") + + # OPTIONAL + def _fetch_parameter_options( + self, parameter: str, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> list[ParameterOption]: + """ + Fetch the parameter options of the trigger. + + Implementation guidelines: + When you need to fetch parameter options from an external service, use the credentials + and credential_type to call the external service API, then return the options to Dify + for user selection. + + Args: + parameter: The parameter name for which to fetch options + credentials: Authentication credentials for the external service + credential_type: The type of credentials (e.g., "api-key", "oauth2", "unauthorized") + + Returns: + list[ParameterOption]: A list of available options for the parameter + + Examples: + GitHub Repositories: + >>> result = provider.fetch_parameter_options(parameter="repository") + >>> print(result) # [ParameterOption(label="owner/repo", value="owner/repo")] + + Slack Channels: + >>> result = provider.fetch_parameter_options(parameter="channel") + >>> print(result) +``` + +### Event + +```Python +class Event(ABC): + @abstractmethod + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + """ + Transform the incoming webhook request into structured Variables. + + This method should: + 1. Parse the webhook payload from the request + 2. Apply filtering logic based on parameters + 3. Extract relevant data matching the output_schema + 4. Return a structured Variables object + + Args: + request: The incoming webhook HTTP request containing the raw payload. + Use request.get_json() to parse JSON body. + parameters: User-configured parameters for filtering and transformation + (e.g., label filters, regex patterns, threshold values). + These come from the subscription configuration. + payload: The decoded payload from previous step `Trigger.dispatch_event`. + It will be delivered into `_on_event` method. + Returns: + Variables: Structured variables matching the output_schema + defined in the event's YAML configuration. + + Raises: + EventIgnoreError: When the event should be filtered out based on parameters + ValueError: When the payload is invalid or missing required fields + + Example: + >>> def _on_event(self, request, parameters): + ... payload = request.get_json() + ... + ... # Apply filters + ... if not self._matches_filters(payload, parameters): + ... raise EventIgnoreError() + ... + ... # Transform data + ... return Variables(variables={ + ... "title": payload["issue"]["title"], + ... "author": payload["issue"]["user"]["login"], + ... "url": payload["issue"]["html_url"], + ... }) + """ + + def _fetch_parameter_options(self, parameter: str) -> list[ParameterOption]: + """ + Fetch the parameter options of the trigger. + + To be implemented by subclasses. + + Also, it's optional to implement, that's why it's not an abstract method. + """ + raise NotImplementedError( + "This plugin should implement `_fetch_parameter_options` method to enable dynamic select parameter" + ) +``` + +{/* +Contributing Section +DO NOT edit this section! +It will be automatically generated by the script. +*/} + +--- + +[Edit this page](https://github.com/langgenius/dify-docs/edit/main/plugin-dev-en/0222-trigger-plugin.mdx) | [Report an issue](https://github.com/langgenius/dify-docs/issues/new?template=docs.yml) + diff --git a/plugin-dev-en/9241-bundle.mdx b/plugin-dev-en/9241-bundle.mdx index a6b8f0b7..8e9cace1 100644 --- a/plugin-dev-en/9241-bundle.mdx +++ b/plugin-dev-en/9241-bundle.mdx @@ -67,7 +67,7 @@ dify-plugin bundle append marketplace . --marketplace_pattern=langgenius/openai: Where `marketplace_pattern` is the reference to the plugin in the marketplace, in the format `organization_name/plugin_name:version_number`. -* **Github** +* **GitHub** Execute the following command: diff --git a/plugin-dev-ja/0131-cheatsheet.mdx b/plugin-dev-ja/0131-cheatsheet.mdx index a8bb5a0d..59f0b50f 100644 --- a/plugin-dev-ja/0131-cheatsheet.mdx +++ b/plugin-dev-ja/0131-cheatsheet.mdx @@ -141,6 +141,10 @@ Dify は複数のタイプのプラグイン開発をサポートしています > 詳細はこちら:[Agent 戦略プラグイン](/plugin-dev-ja/9433-agent-strategy-plugin) - **拡張プラグイン**: Dify プラットフォーム機能を拡張(例:Endpoint および WebAPP) > 詳細はこちら:[拡張プラグイン](/plugin-dev-ja/9231-extension-plugin) +- **データソースプラグイン**:ナレッジパイプラインにおいて、ドキュメントデータのソースとして機能し、パイプライン全体の起点となる + > 詳細はこちら:[データソースプラグイン](/plugin-dev-ja/0222-datasource-plugin) +- **トリガープラグイン**:外部イベント発生時にWorkflowを自動実行 + > 詳細はこちら:[トリガープラグイン](/plugin-dev-ja/0222-trigger-plugin) {/* Contributing Section diff --git a/plugin-dev-ja/0222-trigger-plugin.mdx b/plugin-dev-ja/0222-trigger-plugin.mdx new file mode 100644 index 00000000..45302628 --- /dev/null +++ b/plugin-dev-ja/0222-trigger-plugin.mdx @@ -0,0 +1,666 @@ +--- +title: "トリガープラグイン" +--- + +## トリガープラグインとは? + +トリガー(Trigger) は、Dify v1.10.0で導入された新しいタイプの開始ノードです。Code、Tool、Knowledge Retrievalなどの機能的なノードとは異なり、トリガーの目的は、**サードパーティのイベントをDifyが認識・処理できる入力形式に変換する**ことです。 + +![トリガープラグインの紹介](/images/trigger_plugin_intro.PNG) + +例えば、Gmailの`new email`イベントの受信先としてDifyを設定すると、新しいメールを受信するたびに、Gmailはワークフローを起動するためのイベントをDifyに自動的に送信します。しかし、これには課題があります。 + +- Gmailの元のイベント形式は、Difyの入力形式と互換性がありません。 + +- 世界中には何千ものプラットフォームが存在し、それぞれが独自のイベント形式を持っています。 + +そのため、さまざまなプラットフォームや形式から送られてくるこれらのイベントを定義・解析し、Difyが受け入れられる統一された入力形式に変換するために、トリガープラグインが必要となります。 + +## 技術概要 + +Difyのトリガーは、Webで広く採用されているメカニズムである**Webhook**に基づいて実装されています。多くの主流SaaSプラットフォーム(GitHub、Slack、Linearなど)は、包括的な開発者向けドキュメントと共にWebhookをサポートしています。 + +Webhookは、HTTPベースのイベントディスパッチャと理解することができます。**イベント受信アドレスを設定すると、これらのSaaSプラットフォームは、登録したイベントが発生するたびに、ターゲットサーバーにイベントデータを自動的にプッシュします。** + +異なるプラットフォームからのWebhookイベントを統一的に処理するために、Difyは**サブスクリプション (Subscription)** と **イベント (Event)** という2つのコアコンセプトを定義しています。 + +- **サブスクリプション**: Webhookベースのイベントディスパッチでは、**サードパーティプラットフォームの開発者コンソールでDifyのネットワークアドレスをターゲットサーバーとして登録する必要があります。Difyでは、この設定プロセスをサブスクリプションと呼びます。** + +- **イベント**: プラットフォームは、*メール受信*、*メール削除*、*メール既読*など、複数のタイプのイベントを送信することがあり、これらはすべて登録されたアドレスにプッシュされます。トリガープラグインは複数のイベントタイプを処理でき、各イベントがDifyワークフロー内のプラグイントリガーノードに対応します。 + +## プラグイン開発 + +トリガープラグインの開発プロセスは、他のプラグインタイプ(Tool、Data Source、Modelなど)と一貫しています。 + +`dify plugin init`コマンドを使用して開発テンプレートを作成できます。生成されるファイル構造は、標準のプラグイン形式仕様に準拠しています。 + +``` +├── _assets +│   └── icon.svg +├── events +│   └── star +│   ├── star_created.py +│   └── star_created.yaml +├── main.py +├── manifest.yaml +├── provider +│   ├── github.py +│   └── github.yaml +├── README.md +├── PRIVACY.md +└── requirements.txt +``` + +- `manifest.yaml`: プラグインの基本的なメタデータを記述します。 + +- `provider` ディレクトリ: プロバイダーのメタデータ、購読を作成するためのコード、Webhookリクエスト受信後にイベントを分類するためのコードが含まれます。 + +- **`events` ディレクトリ: イベント処理とフィルタリングのコードが含まれ、ノードレベルでのローカルイベントフィルタリングをサポートします。関連するイベントをグループ化するためにサブディレクトリを作成できます。** + + + トリガープラグインの場合、要求されるDifyの最小バージョンは`1.10.0`、SDKバージョンは`>= 0.6.0`に設定する必要があります。 + + +次に、GitHubを例として、トリガープラグインの開発プロセスを説明します。 + +### Subscription(購読)の作成 + +Webhookの設定方法は、主流のSaaSプラットフォーム間で大きく異なります。 + +- 一部のプラットフォーム(GitHubなど)は、APIベースのWebhook設定をサポートしています。これらのプラットフォームでは、OAuth認証が完了すると、Difyが自動的にWebhookをセットアップできます。 + +- 他のプラットフォーム(Notionなど)は、Webhook設定APIを提供しておらず、ユーザーによる手動認証が必要な場合があります。 + +これらの違いに対応するため、サブスクリプションのプロセスを**サブスクリプションコンストラクタ (Subscription Constructor)** と **サブスクリプション (Subscription)** 自体の2つの部分に分けます。 + +Notionのようなプラットフォームでは、サブスクリプションを作成するために、ユーザーがDifyから提供されたコールバックURLを手動でコピーし、Notionワークスペースに貼り付けてWebhook設定を完了させる必要があります。このプロセスは、Difyインターフェースの**URLを貼り付けて新しいサブスクリプションを作成**オプションに対応します。 + +URLを貼り付けて新しいサブスクリプションを作成 + +手動でのURL貼り付けによるサブスクリプション作成を実装するには、`github.yaml`と`github.py`の2つのファイルを変更する必要があります。 + + + + + GitHubのWebhookは暗号化メカニズムを使用しているため、受信リクエストを復号化して検証するにはシークレットキーが必要です。そのため、`github.yaml`で`webhook_secret`を宣言する必要があります。 + + ```YAML + subscription_schema: + - name: "webhook_secret" + type: "secret-input" + required: false + label: + zh_Hans: "Webhook Secret" + en_US: "Webhook Secret" + ja_JP: "Webhookシークレット" + help: + en_US: "Optional webhook secret for validating GitHub webhook requests" + ja_JP: "GitHub Webhookリクエストの検証用のオプションのWebhookシークレット" + zh_Hans: "可选的用于验证 GitHub webhook 请求的 webhook 密钥" + ``` + + + + まず、`dispatch_event`インターフェースを実装する必要があります。コールバックURLに送信されるすべてのリクエストはこのインターフェースによって処理され、処理されたイベントはデバッグと検証のために**リクエストログ**セクションに表示されます。 + + 手動セットアップ + + コード内では、`github.yaml`で宣言された`webhook_secret`を`subscription.properties`経由で取得できます。 + + `dispatch_event`メソッドは、リクエストの内容に基づいてイベントタイプを決定する必要があります。以下の例では、このイベント抽出は`_dispatch_trigger_event`メソッドによって処理されます。 + + + 完全なコードサンプルについては、[DifyのGitHubトリガープラグイン](https://github.com/langgenius/dify-plugin-sdks/tree/feat/trigger/python/examples/github_trigger)を参照してください。 + + + ```Python + class GithubTrigger(Trigger): + """Handle GitHub webhook event dispatch.""" + + def _dispatch_event(self, subscription: Subscription, request: Request) -> EventDispatch: + webhook_secret = subscription.properties.get("webhook_secret") + if webhook_secret: + self._validate_signature(request=request, webhook_secret=webhook_secret) + + event_type: str | None = request.headers.get("X-GitHub-Event") + if not event_type: + raise TriggerDispatchError("Missing GitHub event type header") + + payload: Mapping[str, Any] = self._validate_payload(request) + response = Response(response='{"status": "ok"}', status=200, mimetype="application/json") + event: str = self._dispatch_trigger_event(event_type=event_type, payload=payload) + return EventDispatch(events=[event] if event else [], response=response) + ``` + + + +### イベント処理 + +イベントが抽出されると、対応する実装は元のHTTPリクエストをフィルタリングし、Difyワークフローが受け入れ可能な入力形式に変換する必要があります。 + +Issueイベントを例にとると、`events/issues/issues.yaml`と`events/issues/issues.py`を通じてイベントとその実装を定義できます。イベントの出力は`issues.yaml`の`output_schema`セクションで定義でき、これはツールプラグインと同じくJSON Schema仕様に従います。 + + + + ```YAML + identity: + name: issues + author: langgenius + label: + en_US: Issues + zh_Hans: 议题 + ja_JP: イシュー + description: + en_US: Unified issues event with actions filter + zh_Hans: 带 actions 过滤的统一 issues 事件 + ja_JP: アクションフィルタ付きの統合イシューイベント + output_schema: + type: object + properties: + action: + type: string + issue: + type: object + description: The issue itself + extra: + python: + source: events/issues/issues.py + ``` + + + ```Python + from collections.abc import Mapping + from typing import Any + + from werkzeug import Request + + from dify_plugin.entities.trigger import Variables + from dify_plugin.errors.trigger import EventIgnoreError + from dify_plugin.interfaces.trigger import Event + + class IssuesUnifiedEvent(Event): + """Unified Issues event. Filters by actions and common issue attributes.""" + + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + payload = request.get_json() + if not payload: + raise ValueError("No payload received") + + allowed_actions = parameters.get("actions") or [] + action = payload.get("action") + if allowed_actions and action not in allowed_actions: + raise EventIgnoreError() + + issue = payload.get("issue") + if not isinstance(issue, Mapping): + raise ValueError("No issue in payload") + + return Variables(variables={**payload}) + ``` + + + +### イベントフィルタリング + +特定のイベント(例えば、特定のラベルを持つIssueイベントのみ)をフィルタリングするには、`issues.yaml`のイベント定義に`parameters`を追加します。その後、`_on_event`メソッド内で`EventIgnoreError`例外をスローすることで、設定された基準を満たさないイベントを除外できます。 + + + + ```YAML + parameters: + - name: added_label + label: + en_US: Added Label + zh_Hans: 添加的标签 + ja_JP: 追加されたラベル + type: string + required: false + description: + en_US: "Only trigger if these specific labels were added (e.g., critical, priority-high, security, comma-separated). Leave empty to trigger for any label addition." + zh_Hans: "仅当添加了这些特定标签时触发(例如:critical, priority-high, security,逗号分隔)。留空则对任何标签添加触发。" + ja_JP: "これらの特定のラベルが追加された場合のみトリガー(例: critical, priority-high, security,カンマ区切り)。空の場合は任意のラベル追加でトリガー。" + ``` + + + ```Python + def _check_added_label(self, payload: Mapping[str, Any], added_label_param: str | None) -> None: + """Check if the added label matches the allowed labels""" + if not added_label_param: + return + + allowed_labels = [label.strip() for label in added_label_param.split(",") if label.strip()] + if not allowed_labels: + return + + # The payload contains the label that was added + label = payload.get("label", {}) + label_name = label.get("name", "") + + if label_name not in allowed_labels: + raise EventIgnoreError() + + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + # ... + # Apply all filters + self._check_added_label(payload, parameters.get("added_label")) + + return Variables(variables={**payload}) + ``` + + + +### OAuthまたはAPIキーによるサブスクリプションの作成 + +OAuthまたはAPIキーを介した自動的なサブスクリプション作成を有効にするには、`github.yaml`と`github.py`ファイルを変更する必要があります。 + + + + `github.yaml`に以下のフィールドを追加します。 + + ```YAML + subscription_constructor: + parameters: + - name: "repository" + label: + en_US: "Repository" + zh_Hans: "仓库" + ja_JP: "リポジトリ" + type: "dynamic-select" + required: true + placeholder: + en_US: "owner/repo" + zh_Hans: "owner/repo" + ja_JP: "owner/repo" + help: + en_US: "GitHub repository in format owner/repo (e.g., microsoft/vscode)" + zh_Hans: "GitHub 仓库,格式为 owner/repo(例如:microsoft/vscode)" + ja_JP: "GitHubリポジトリは owner/repo 形式で入力してください(例: microsoft/vscode)" + credentials_schema: + access_tokens: + help: + en_US: Get your Access Tokens from GitHub + ja_JP: GitHub からアクセストークンを取得してください + zh_Hans: 从 GitHub 获取您的 Access Tokens + label: + en_US: Access Tokens + ja_JP: アクセストークン + zh_Hans: Access Tokens + placeholder: + en_US: Please input your GitHub Access Tokens + ja_JP: GitHub のアクセストークンを入力してください + zh_Hans: 请输入你的 GitHub Access Tokens + required: true + type: secret-input + url: https://github.com/settings/tokens?type=beta + extra: + python: + source: provider/github.py + ``` + + `subscription_constructor`は、サブスクリプションがどのように構築されるかを定義するためにDifyによって抽象化された概念です。以下のフィールドが含まれます。 + + - `parameters` (任意): 購読するイベントタイプやターゲットのGitHubリポジトリなど、サブスクリプション作成に必要なパラメータを定義します。 + + - `credentials_schema` (任意): APIキーでサブスクリプションを作成する際に必要な認証情報(例: GitHubの`access_tokens`)を宣言します。 + + - `oauth_schema` (任意): OAuth経由でのサブスクリプション作成を実装する場合に必要です。定義方法の詳細は、[ツールプラグインにOAuthサポートを追加する](/plugin-dev-en/0222-tool-oauth)を参照してください。 + + + `github.py`に`Constructor`クラスを作成し、自動サブスクリプションロジックを実装します。 + + ```Python + class GithubSubscriptionConstructor(TriggerSubscriptionConstructor): + """Manage GitHub trigger subscriptions.""" + def _validate_api_key(self, credentials: Mapping[str, Any]) -> None: + # ... + + def _create_subscription( + self, + endpoint: str, + parameters: Mapping[str, Any], + credentials: Mapping[str, Any], + credential_type: CredentialType, + ) -> Subscription: + repository = parameters.get("repository") + if not repository: + raise ValueError("repository is required (format: owner/repo)") + + try: + owner, repo = repository.split("/") + except ValueError: + raise ValueError("repository must be in format 'owner/repo'") from None + + events: list[str] = parameters.get("events", []) + webhook_secret = uuid.uuid4().hex + 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": endpoint, "content_type": "json", "insecure_ssl": "0", "secret": webhook_secret}, + } + + try: + response = requests.post(url, json=webhook_data, headers=headers, timeout=10) + except requests.RequestException as exc: + raise SubscriptionError(f"Network error while creating webhook: {exc}", error_code="NETWORK_ERROR") from exc + + if response.status_code == 201: + webhook = response.json() + return Subscription( + expires_at=int(time.time()) + self._WEBHOOK_TTL, + endpoint=endpoint, + parameters=parameters, + properties={ + "external_id": str(webhook["id"]), + "repository": repository, + "events": events, + "webhook_secret": webhook_secret, + "active": webhook.get("active", True), + }, + ) + + response_data: dict[str, Any] = response.json() if response.content else {} + error_msg = response_data.get("message", "Unknown error") + error_details = response_data.get("errors", []) + detailed_error = f"Failed to create GitHub webhook: {error_msg}" + if error_details: + detailed_error += f" Details: {error_details}" + + raise SubscriptionError( + detailed_error, + error_code="WEBHOOK_CREATION_FAILED", + external_response=response_data, + ) + ``` + + + +--- + + +これら2つのファイルを変更すると、Difyインターフェースに**APIキーで作成**オプションが表示されます。 + +OAuth経由の自動サブスクリプション作成も同じ`Constructor`クラスで実装できます。`subscription_constructor`の下に`oauth_schema`フィールドを追加することで、OAuth認証を有効にできます。 + +OAuthとAPIキーのオプション + +## さらに詳しく + +トリガープラグイン開発における核心クラスのインターフェース定義と実装方法は以下の通りです。 + +### Trigger + +```Python +class Trigger(ABC): + @abstractmethod + def _dispatch_event(self, subscription: Subscription, request: Request) -> EventDispatch: + """ + Internal method to implement event dispatch logic. + + Subclasses must override this method to handle incoming webhook events. + + Implementation checklist: + 1. Validate the webhook request: + - Check signature/HMAC using properties when you create the subscription from subscription.properties + - Verify request is from expected source + 2. Extract event information: + - Parse event type from headers or body + - Extract relevant payload data + 3. Return EventDispatch with: + - events: List of Event names to invoke (can be single or multiple) + - response: Appropriate HTTP response for the webhook + + Args: + subscription: The Subscription object with endpoint and properties fields + request: Incoming webhook HTTP request + + Returns: + EventDispatch: Event dispatch routing information + + Raises: + TriggerValidationError: For security validation failures + TriggerDispatchError: For parsing or routing errors + """ + raise NotImplementedError("This plugin should implement `_dispatch_event` method to enable event dispatch") + +``` + +### TriggerSubscriptionConstructor + +```Python +class TriggerSubscriptionConstructor(ABC, OAuthProviderProtocol): + # OPTIONAL + def _validate_api_key(self, credentials: Mapping[str, Any]) -> None: + raise NotImplementedError( + "This plugin should implement `_validate_api_key` method to enable credentials validation" + ) + + # OPTIONAL + def _oauth_get_authorization_url(self, redirect_uri: str, system_credentials: Mapping[str, Any]) -> str: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_get_authorization_url` method" + ) + + # OPTIONAL + def _oauth_get_credentials( + self, redirect_uri: str, system_credentials: Mapping[str, Any], request: Request + ) -> TriggerOAuthCredentials: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_get_credentials` method" + ) + + # OPTIONAL + def _oauth_refresh_credentials( + self, redirect_uri: str, system_credentials: Mapping[str, Any], credentials: Mapping[str, Any] + ) -> OAuthCredentials: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_refresh_credentials` method" + ) + + @abstractmethod + def _create_subscription( + self, + endpoint: str, + parameters: Mapping[str, Any], + credentials: Mapping[str, Any], + credential_type: CredentialType, + ) -> Subscription: + """ + Internal method to implement subscription logic. + + Subclasses must override this method to handle subscription creation. + + Implementation checklist: + 1. Use the endpoint parameter provided by Dify + 2. Register webhook with external service using their API + 3. Store all necessary information in Subscription.properties for future operations(e.g., dispatch_event) + 4. Return Subscription with: + - expires_at: Set appropriate expiration time + - endpoint: The webhook endpoint URL allocated by Dify for receiving events, same with the endpoint parameter + - parameters: The parameters of the subscription + - properties: All configuration and external IDs + + Args: + endpoint: The webhook endpoint URL allocated by Dify for receiving events + parameters: Subscription creation parameters + credentials: Authentication credentials + credential_type: The type of the credentials, e.g., "api-key", "oauth2", "unauthorized" + + Returns: + Subscription: Subscription details with metadata for future operations + + Raises: + SubscriptionError: For operational failures (API errors, invalid credentials) + ValueError: For programming errors (missing required params) + """ + raise NotImplementedError( + "This plugin should implement `_create_subscription` method to enable event subscription" + ) + + @abstractmethod + def _delete_subscription( + self, subscription: Subscription, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> UnsubscribeResult: + """ + Internal method to implement unsubscription logic. + + Subclasses must override this method to handle subscription removal. + + Implementation guidelines: + 1. Extract necessary IDs from subscription.properties (e.g., external_id) + 2. Use credentials and credential_type to call external service API to delete the webhook + 3. Handle common errors (not found, unauthorized, etc.) + 4. Always return UnsubscribeResult with detailed status + 5. Never raise exceptions for operational failures - use UnsubscribeResult.success=False + + Args: + subscription: The Subscription object with endpoint and properties fields + + Returns: + UnsubscribeResult: Always returns result, never raises for operational failures + """ + raise NotImplementedError( + "This plugin should implement `_delete_subscription` method to enable event unsubscription" + ) + + @abstractmethod + def _refresh_subscription( + self, subscription: Subscription, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> Subscription: + """ + Internal method to implement subscription refresh logic. + + Subclasses must override this method to handle simple expiration extension. + + Implementation patterns: + 1. For webhooks without expiration (e.g., GitHub): + - Update the Subscription.expires_at=-1 then Dify will never call this method again + + 2. For lease-based subscriptions (e.g., Microsoft Graph): + - Use the information in Subscription.properties to call service's lease renewal API if available + - Handle renewal limits (some services limit renewal count) + - Update the Subscription.properties and Subscription.expires_at for next time renewal if needed + + Args: + subscription: Current subscription with properties + credential_type: The type of the credentials, e.g., "api-key", "oauth2", "unauthorized" + credentials: Current authentication credentials from credentials_schema. + For API key auth, according to `credentials_schema` defined in the YAML. + For OAuth auth, according to `oauth_schema.credentials_schema` defined in the YAML. + For unauthorized auth, there is no credentials. + + Returns: + Subscription: Same subscription with extended expiration + or new properties and expires_at for next time renewal + + Raises: + SubscriptionError: For operational failures (API errors, invalid credentials) + """ + raise NotImplementedError("This plugin should implement `_refresh` method to enable subscription refresh") + + # OPTIONAL + def _fetch_parameter_options( + self, parameter: str, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> list[ParameterOption]: + """ + Fetch the parameter options of the trigger. + + Implementation guidelines: + When you need to fetch parameter options from an external service, use the credentials + and credential_type to call the external service API, then return the options to Dify + for user selection. + + Args: + parameter: The parameter name for which to fetch options + credentials: Authentication credentials for the external service + credential_type: The type of credentials (e.g., "api-key", "oauth2", "unauthorized") + + Returns: + list[ParameterOption]: A list of available options for the parameter + + Examples: + GitHub Repositories: + >>> result = provider.fetch_parameter_options(parameter="repository") + >>> print(result) # [ParameterOption(label="owner/repo", value="owner/repo")] + + Slack Channels: + >>> result = provider.fetch_parameter_options(parameter="channel") + >>> print(result) +``` + +### Event + +```Python +class Event(ABC): + @abstractmethod + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + """ + Transform the incoming webhook request into structured Variables. + + This method should: + 1. Parse the webhook payload from the request + 2. Apply filtering logic based on parameters + 3. Extract relevant data matching the output_schema + 4. Return a structured Variables object + + Args: + request: The incoming webhook HTTP request containing the raw payload. + Use request.get_json() to parse JSON body. + parameters: User-configured parameters for filtering and transformation + (e.g., label filters, regex patterns, threshold values). + These come from the subscription configuration. + payload: The decoded payload from previous step `Trigger.dispatch_event`. + It will be delivered into `_on_event` method. + Returns: + Variables: Structured variables matching the output_schema + defined in the event's YAML configuration. + + Raises: + EventIgnoreError: When the event should be filtered out based on parameters + ValueError: When the payload is invalid or missing required fields + + Example: + >>> def _on_event(self, request, parameters): + ... payload = request.get_json() + ... + ... # Apply filters + ... if not self._matches_filters(payload, parameters): + ... raise EventIgnoreError() + ... + ... # Transform data + ... return Variables(variables={ + ... "title": payload["issue"]["title"], + ... "author": payload["issue"]["user"]["login"], + ... "url": payload["issue"]["html_url"], + ... }) + """ + + def _fetch_parameter_options(self, parameter: str) -> list[ParameterOption]: + """ + Fetch the parameter options of the trigger. + + To be implemented by subclasses. + + Also, it's optional to implement, that's why it's not an abstract method. + """ + raise NotImplementedError( + "This plugin should implement `_fetch_parameter_options` method to enable dynamic select parameter" + ) +``` + +{/* +Contributing Section +DO NOT edit this section! +It will be automatically generated by the script. +*/} + +--- + +[このページを編集する](https://github.com/langgenius/dify-docs/edit/main/plugin-dev-ja/0222-trigger-plugin.mdx) | [問題を報告する](https://github.com/langgenius/dify-docs/issues/new?template=docs.yml) + diff --git a/plugin-dev-ja/9241-bundle.mdx b/plugin-dev-ja/9241-bundle.mdx index 41b5b673..4e3a81e7 100644 --- a/plugin-dev-ja/9241-bundle.mdx +++ b/plugin-dev-ja/9241-bundle.mdx @@ -65,7 +65,7 @@ dify-plugin bundle append marketplace . --marketplace_pattern=langgenius/openai: ここで、`marketplace_pattern` は Marketplace でのプラグイン参照であり、フォーマットは `組織名/プラグイン名:バージョン番号` です。 -* **Github** +* **GitHub** 次のコマンドを実行します。 diff --git a/plugin-dev-zh/0131-cheatsheet.mdx b/plugin-dev-zh/0131-cheatsheet.mdx index 2b96abc1..a2ae7c0c 100644 --- a/plugin-dev-zh/0131-cheatsheet.mdx +++ b/plugin-dev-zh/0131-cheatsheet.mdx @@ -135,14 +135,18 @@ class ToolLabelEnum(Enum): Dify 支持多种类型的插件开发: -- **工具插件**: 集成第三方 API 和服务 +- **工具插件**:集成第三方 API 和服务 > 了解更多:[工具插件开发](/plugin-dev-zh/0211-getting-started-dify-tool) -- **模型插件**: 集成 AI 模型 +- **模型插件**:集成 AI 模型 > 了解更多:[模型插件介绍](/plugin-dev-zh/0131-model-plugin-introduction)、[快速接入一个新模型](/plugin-dev-zh/0211-getting-started-new-model) -- **Agent 策略插件**: 自定义 Agent 思考和决策策略 +- **Agent 策略插件**:自定义 Agent 思考和决策策略 > 了解更多:[Agent 策略插件](/plugin-dev-zh/9433-agent-strategy-plugin) -- **扩展插件**: 扩展 Dify 平台功能,例如 Endpoint 和 WebAPP +- **扩展插件**:扩展 Dify 平台功能,例如 Endpoint 和 WebAPP > 了解更多:[扩展插件](/plugin-dev-zh/9231-extension-plugin) +- **数据源插件**:在知识流水线中,作为文档数据的来源并充当整个流水线的起始点 + > 了解更多:[数据源插件](/plugin-dev-zh/0222-datasource-plugin) +- **触发器插件**:外部事件发生时,自动触发 Workflow 运行 + > 了解更多:[触发器插件](/plugin-dev-zh/0222-trigger-plugin) {/* Contributing Section diff --git a/plugin-dev-zh/0222-datasource-plugin.mdx b/plugin-dev-zh/0222-datasource-plugin.mdx index 887c6e22..d16360df 100644 --- a/plugin-dev-zh/0222-datasource-plugin.mdx +++ b/plugin-dev-zh/0222-datasource-plugin.mdx @@ -110,7 +110,7 @@ datasources: - 数据源插件支持以 OAuth 2.0 或 API Key 两种方式进行认证。了解如何配置 OAuth,请阅读 [为工具插件添加 OAuth 支持](/plugin-dev-zh/tool-oauth)。 + 数据源插件支持以 OAuth 2.0 或 API Key 两种方式进行认证。了解如何配置 OAuth,请阅读 [为工具插件添加 OAuth 支持](/plugin-dev-zh/0222-tool-oauth)。 #### 创建供应商代码文件 diff --git a/plugin-dev-zh/0222-trigger-plugin.mdx b/plugin-dev-zh/0222-trigger-plugin.mdx new file mode 100644 index 00000000..be402f41 --- /dev/null +++ b/plugin-dev-zh/0222-trigger-plugin.mdx @@ -0,0 +1,660 @@ +--- +title: "触发器插件" +--- + +## 触发器插件是什么? + +Dify v1.10.0 引入了触发器(Trigger)类型的开始节点。区别于代码(Code)、工具(Tool)、知识检索(Knowledge Retrieval)等功能性节点,触发器的作用是**将第三方事件转化为 Dify 能接受的入参格式**。 + +![Trigger Plugin Intro](/images/trigger_plugin_intro.PNG) + +例如,在 Gmail 中将事件的接收方配置为 Dify 后,每当你收到一封新邮件,Gmail 就会向 Dify 发送一个事件,可用于触发工作流。然而: + +- Gmail 的原始请求格式不是 Dify 可接受的格式; + +- 全世界有成千上万的平台,每个平台的格式都不尽相同。 + +因此,我们需要触发器插件来定义和解析这些来自不同平台、不同格式的事件,并将它们统一为 Dify 可接受的标准入参格式。 + +## 技术原理 + +Dify 触发器基于 Webhook 实现。Webhook 是互联网中广泛应用的机制,主流 SaaS 平台(如 Github、Slack、Linear 等)均支持 Webhook 并配有详细的开发文档。 + +Webhook 可理解为基于 HTTP 协议的事件派发中心。**当配置好事件接收地址后,这些 SaaS 平台会在指定事件发生时自动将其推送至目标服务器。** + +为了统一处理来自不同平台的 Webhook 事件,Dify 抽象出两个核心概念:订阅(Subscription) 和事件(Event)。 + +- **订阅**:如前所述,基于 Webhook 的事件派发有一个前提条件——**需要将 Dify 的网络地址作为目标服务器,配置在第三方平台的开发者后台中。这个配置过程在 Dify 中称为「订阅」。** + +- **事件**:一个平台可能会发送多种类型的事件,例如 *新邮件*、*邮件删除*、*邮件已读* 等,这些事件均会被推送至预先配置的地址。一个触发器插件可配置多个事件,一个事件对应 Dify 工作流中的一个触发器节点。 + +## 插件开发 + +触发器插件的开发方式与工具(Tool)、数据源(Data Source)、模型(Model)等插件类型一致,均可通过 `dify plugin init` 命令创建开发模板,其文件结构遵循统一的插件格式规范: + +``` +├── _assets +│   └── icon.svg +├── events +│   └── star +│   ├── star_created.py +│   └── star_created.yaml +├── main.py +├── manifest.yaml +├── provider +│   ├── github.py +│   └── github.yaml +├── README.md +├── PRIVACY.md +└── requirements.txt +``` +- `manifest.yaml`:描述插件的基本信息。 + +- `provider` 目录:包含插件供应商的描述信息、创建订阅的代码,以及接收 Webhook 请求后对事件进行分类的代码。 + +- **`events` 目录:包含事件处理和筛选功能的代码,支持在节点端对事件进行本地筛选;每个目录下可创建二级目录,以便对事件进行分组。** + + + 在触发器插件中,最低 Dify 版本需设置为 `1.10.0`,SDK 版本需设置为大于等于 `0.6.0`。 + + +下面以 GitHub 为例,介绍触发器插件的具体开发方法。 + +### 订阅创建 + +在主流 SaaS 平台中,Webhook 的配置方式差异较大: + +- 部分平台(如 GitHub)支持通过 API 配置 Webhook。此类平台在 Dify 中完成 OAuth 鉴权后,Dify 即可自动完成 Webhook 配置。 + +- 另一类平台(如 Notion)不仅不提供 Webhook API 配置功能,还要求用户手动完成部分校验工作。 + +因此,我们将订阅过程分为两个部分:Subscription Constructor 和 Subscription。 + +对于 Notion 这类平台,订阅的创建需要用户手动将 Dify 提供的回调地址(Callback URL)复制粘贴到 Notion 后台中,以完成 Webhook 配置。此流程对应 Dify 界面中的 **粘贴 URL 以创建新订阅** 选项。 + +Paste URL to Create a New Subscription + +若要实现通过手动复制 Callback URL 的方式创建订阅,需要修改 `github.yaml` 和 `github.py` 两个文件。 + + + + 由于 Github Webhook 存在加密机制,解密和校验请求需要使用密钥,因此需在 `github.yaml` 中声明 `webhook_secret`。 + + ```YAML + subscription_schema: + - name: "webhook_secret" + type: "secret-input" + required: false + label: + zh_Hans: "Webhook Secret" + en_US: "Webhook Secret" + ja_JP: "Webhookシークレット" + help: + en_US: "Optional webhook secret for validating GitHub webhook requests" + ja_JP: "GitHub Webhookリクエストの検証用のオプションのWebhookシークレット" + zh_Hans: "可选的用于验证 GitHub webhook 请求的 webhook 密钥" + ``` + + + + 首先,我们需要编写 `dispatch_event` 接口。所有发送到 Callback URL 的请求都会先经过该接口处理,处理后的事件将显示在界面上的 **请求日志** 中,便于调试与验证。 + + Manual Setup + + 在代码中,可通过 `subscription.properties` 获取在 `github.yaml` 中声明的 `webhook_secret`。 + + `dispatch_event` 方法需要根据请求内容判断该请求对应的事件类型。在以下示例中,事件提取由 `_dispatch_trigger_event` 方法完成。 + + + 完整代码示例,请参考 [Dify GitHub 插件代码](https://github.com/langgenius/dify-plugin-sdks/tree/feat/trigger/python/examples/github_trigger)。 + + + ```Python + class GithubTrigger(Trigger): + """Handle GitHub webhook event dispatch.""" + + def _dispatch_event(self, subscription: Subscription, request: Request) -> EventDispatch: + webhook_secret = subscription.properties.get("webhook_secret") + if webhook_secret: + self._validate_signature(request=request, webhook_secret=webhook_secret) + + event_type: str | None = request.headers.get("X-GitHub-Event") + if not event_type: + raise TriggerDispatchError("Missing GitHub event type header") + + payload: Mapping[str, Any] = self._validate_payload(request) + response = Response(response='{"status": "ok"}', status=200, mimetype="application/json") + event: str = self._dispatch_trigger_event(event_type=event_type, payload=payload) + return EventDispatch(events=[event] if event else [], response=response) + ``` + + + +### 事件处理 + +提取出事件后,需要由相应的事件实现对原始 HTTP 请求进行过滤,并将其转换为 Dify 工作流可接受的入参。 + +以 Issue 事件为例,可分别通过 `events/issues/issues.yaml` 和 `events/issues/issues.py` 来定义事件与实现事件。事件的输出可在 `issues.yaml` 的 `output_schema` 中定义,与工具类型插件相同,遵循 JSON Schema 规范。 + + + + ```YAML + identity: + name: issues + author: langgenius + label: + en_US: Issues + zh_Hans: 议题 + ja_JP: イシュー + description: + en_US: Unified issues event with actions filter + zh_Hans: 带 actions 过滤的统一 issues 事件 + ja_JP: アクションフィルタ付きの統合イシューイベント + output_schema: + type: object + properties: + action: + type: string + issue: + type: object + description: The issue itself + extra: + python: + source: events/issues/issues.py + ``` + + + ```Python + from collections.abc import Mapping + from typing import Any + + from werkzeug import Request + + from dify_plugin.entities.trigger import Variables + from dify_plugin.errors.trigger import EventIgnoreError + from dify_plugin.interfaces.trigger import Event + + class IssuesUnifiedEvent(Event): + """Unified Issues event. Filters by actions and common issue attributes.""" + + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + payload = request.get_json() + if not payload: + raise ValueError("No payload received") + + allowed_actions = parameters.get("actions") or [] + action = payload.get("action") + if allowed_actions and action not in allowed_actions: + raise EventIgnoreError() + + issue = payload.get("issue") + if not isinstance(issue, Mapping): + raise ValueError("No issue in payload") + + return Variables(variables={**payload}) + ``` + + + +### 事件过滤 + +若希望插件能够筛选掉部分事件,例如只关注具有某个特定标签的 Issue 事件,可在事件定义中为 Issue 事件添加 `parameters`。在 `_on_event` 方法中,通过抛出 `EventIgnoreError` 异常,即可在实际运行中根据配置的参数过滤掉不符合条件的事件。 + + + + ```YAML + parameters: + - name: added_label + label: + en_US: Added Label + zh_Hans: 添加的标签 + ja_JP: 追加されたラベル + type: string + required: false + description: + en_US: "Only trigger if these specific labels were added (e.g., critical, priority-high, security, comma-separated). Leave empty to trigger for any label addition." + zh_Hans: "仅当添加了这些特定标签时触发(例如:critical, priority-high, security,逗号分隔)。留空则对任何标签添加触发。" + ja_JP: "これらの特定のラベルが追加された場合のみトリガー(例: critical, priority-high, security,カンマ区切り)。空の場合は任意のラベル追加でトリガー。" + ``` + + + ```Python + def _check_added_label(self, payload: Mapping[str, Any], added_label_param: str | None) -> None: + """Check if the added label matches the allowed labels""" + if not added_label_param: + return + + allowed_labels = [label.strip() for label in added_label_param.split(",") if label.strip()] + if not allowed_labels: + return + + # The payload contains the label that was added + label = payload.get("label", {}) + label_name = label.get("name", "") + + if label_name not in allowed_labels: + raise EventIgnoreError() + + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + # ... + # Apply all filters + self._check_added_label(payload, parameters.get("added_label")) + + return Variables(variables={**payload}) + ``` + + + +### 通过 OAuth 或 API key 创建订阅 + +若要实现通过 OAuth 或 API key 自动创建订阅,同样需要修改 `github.yaml` 和 `github.py` 两个文件。 + + + + 在 `github.yaml` 中,添加如下字段。 + + ```YAML + subscription_constructor: + parameters: + - name: "repository" + label: + en_US: "Repository" + zh_Hans: "仓库" + ja_JP: "リポジトリ" + type: "dynamic-select" + required: true + placeholder: + en_US: "owner/repo" + zh_Hans: "owner/repo" + ja_JP: "owner/repo" + help: + en_US: "GitHub repository in format owner/repo (e.g., microsoft/vscode)" + zh_Hans: "GitHub 仓库,格式为 owner/repo(例如:microsoft/vscode)" + ja_JP: "GitHubリポジトリは owner/repo 形式で入力してください(例: microsoft/vscode)" + credentials_schema: + access_tokens: + help: + en_US: Get your Access Tokens from GitHub + ja_JP: GitHub からアクセストークンを取得してください + zh_Hans: 从 GitHub 获取您的 Access Tokens + label: + en_US: Access Tokens + ja_JP: アクセストークン + zh_Hans: Access Tokens + placeholder: + en_US: Please input your GitHub Access Tokens + ja_JP: GitHub のアクセストークンを入力してください + zh_Hans: 请输入你的 GitHub Access Tokens + required: true + type: secret-input + url: https://github.com/settings/tokens?type=beta + extra: + python: + source: provider/github.py + ``` + `subscription_constructor` 是 Dify 抽象出的一个概念,用于定义如何构建订阅,包含以下字段: + + - `parameters`(可选):用于定义创建订阅时所需的参数,例如想要订阅的事件类型或目标 GitHub 仓库等。 + + - `credentials_schema`(可选):通过 API key 创建订阅时,必须填写该字段以声明所需的密钥,例如在 GitHub 中定义 `access_tokens`。 + + - `oauth_schema`(可选):通过 OAuth 创建订阅时,必须填写该字段以启用 OAuth 鉴权。具体定义方式,可参考 [为插件添加 OAuth 支持](/plugin-dev-zh/0222-tool-oauth)。 + + + 在 `github.py` 中创建一个 `Constructor` 类,以实现自动化订阅逻辑。 + + ```Python + class GithubSubscriptionConstructor(TriggerSubscriptionConstructor): + """Manage GitHub trigger subscriptions.""" + def _validate_api_key(self, credentials: Mapping[str, Any]) -> None: + # ... + + def _create_subscription( + self, + endpoint: str, + parameters: Mapping[str, Any], + credentials: Mapping[str, Any], + credential_type: CredentialType, + ) -> Subscription: + repository = parameters.get("repository") + if not repository: + raise ValueError("repository is required (format: owner/repo)") + + try: + owner, repo = repository.split("/") + except ValueError: + raise ValueError("repository must be in format 'owner/repo'") from None + + events: list[str] = parameters.get("events", []) + webhook_secret = uuid.uuid4().hex + 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": endpoint, "content_type": "json", "insecure_ssl": "0", "secret": webhook_secret}, + } + + try: + response = requests.post(url, json=webhook_data, headers=headers, timeout=10) + except requests.RequestException as exc: + raise SubscriptionError(f"Network error while creating webhook: {exc}", error_code="NETWORK_ERROR") from exc + + if response.status_code == 201: + webhook = response.json() + return Subscription( + expires_at=int(time.time()) + self._WEBHOOK_TTL, + endpoint=endpoint, + parameters=parameters, + properties={ + "external_id": str(webhook["id"]), + "repository": repository, + "events": events, + "webhook_secret": webhook_secret, + "active": webhook.get("active", True), + }, + ) + + response_data: dict[str, Any] = response.json() if response.content else {} + error_msg = response_data.get("message", "Unknown error") + error_details = response_data.get("errors", []) + detailed_error = f"Failed to create GitHub webhook: {error_msg}" + if error_details: + detailed_error += f" Details: {error_details}" + + raise SubscriptionError( + detailed_error, + error_code="WEBHOOK_CREATION_FAILED", + external_response=response_data, + ) + ``` + + + +--- + +完成以上两个文件的配置后,你将在 Dify 界面中看到 **通过 API Key 创建** 选项。 + +通过 OAuth 的订阅自动创建也可以在 `Constructor` 中实现:在 `subscription_constructor` 中添加 `oauth_schema` 字段,即可启用 OAuth 认证方式。 + +![OAuth & API Key Options](/images/trigger_plugin_oauth_apikey.png) + +## 探索更多 + +触发器插件开发中核心类的接口定义和实现方法如下。 + +### Trigger + +```Python +class Trigger(ABC): + @abstractmethod + def _dispatch_event(self, subscription: Subscription, request: Request) -> EventDispatch: + """ + Internal method to implement event dispatch logic. + + Subclasses must override this method to handle incoming webhook events. + + Implementation checklist: + 1. Validate the webhook request: + - Check signature/HMAC using properties when you create the subscription from subscription.properties + - Verify request is from expected source + 2. Extract event information: + - Parse event type from headers or body + - Extract relevant payload data + 3. Return EventDispatch with: + - events: List of Event names to invoke (can be single or multiple) + - response: Appropriate HTTP response for the webhook + + Args: + subscription: The Subscription object with endpoint and properties fields + request: Incoming webhook HTTP request + + Returns: + EventDispatch: Event dispatch routing information + + Raises: + TriggerValidationError: For security validation failures + TriggerDispatchError: For parsing or routing errors + """ + raise NotImplementedError("This plugin should implement `_dispatch_event` method to enable event dispatch") + +``` + +### TriggerSubscriptionConstructor + +```Python +class TriggerSubscriptionConstructor(ABC, OAuthProviderProtocol): + # OPTIONAL + def _validate_api_key(self, credentials: Mapping[str, Any]) -> None: + raise NotImplementedError( + "This plugin should implement `_validate_api_key` method to enable credentials validation" + ) + + # OPTIONAL + def _oauth_get_authorization_url(self, redirect_uri: str, system_credentials: Mapping[str, Any]) -> str: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_get_authorization_url` method" + ) + + # OPTIONAL + def _oauth_get_credentials( + self, redirect_uri: str, system_credentials: Mapping[str, Any], request: Request + ) -> TriggerOAuthCredentials: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_get_credentials` method" + ) + + # OPTIONAL + def _oauth_refresh_credentials( + self, redirect_uri: str, system_credentials: Mapping[str, Any], credentials: Mapping[str, Any] + ) -> OAuthCredentials: + raise NotImplementedError( + "The trigger you are using does not support OAuth, please implement `_oauth_refresh_credentials` method" + ) + + @abstractmethod + def _create_subscription( + self, + endpoint: str, + parameters: Mapping[str, Any], + credentials: Mapping[str, Any], + credential_type: CredentialType, + ) -> Subscription: + """ + Internal method to implement subscription logic. + + Subclasses must override this method to handle subscription creation. + + Implementation checklist: + 1. Use the endpoint parameter provided by Dify + 2. Register webhook with external service using their API + 3. Store all necessary information in Subscription.properties for future operations(e.g., dispatch_event) + 4. Return Subscription with: + - expires_at: Set appropriate expiration time + - endpoint: The webhook endpoint URL allocated by Dify for receiving events, same with the endpoint parameter + - parameters: The parameters of the subscription + - properties: All configuration and external IDs + + Args: + endpoint: The webhook endpoint URL allocated by Dify for receiving events + parameters: Subscription creation parameters + credentials: Authentication credentials + credential_type: The type of the credentials, e.g., "api-key", "oauth2", "unauthorized" + + Returns: + Subscription: Subscription details with metadata for future operations + + Raises: + SubscriptionError: For operational failures (API errors, invalid credentials) + ValueError: For programming errors (missing required params) + """ + raise NotImplementedError( + "This plugin should implement `_create_subscription` method to enable event subscription" + ) + + @abstractmethod + def _delete_subscription( + self, subscription: Subscription, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> UnsubscribeResult: + """ + Internal method to implement unsubscription logic. + + Subclasses must override this method to handle subscription removal. + + Implementation guidelines: + 1. Extract necessary IDs from subscription.properties (e.g., external_id) + 2. Use credentials and credential_type to call external service API to delete the webhook + 3. Handle common errors (not found, unauthorized, etc.) + 4. Always return UnsubscribeResult with detailed status + 5. Never raise exceptions for operational failures - use UnsubscribeResult.success=False + + Args: + subscription: The Subscription object with endpoint and properties fields + + Returns: + UnsubscribeResult: Always returns result, never raises for operational failures + """ + raise NotImplementedError( + "This plugin should implement `_delete_subscription` method to enable event unsubscription" + ) + + @abstractmethod + def _refresh_subscription( + self, subscription: Subscription, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> Subscription: + """ + Internal method to implement subscription refresh logic. + + Subclasses must override this method to handle simple expiration extension. + + Implementation patterns: + 1. For webhooks without expiration (e.g., GitHub): + - Update the Subscription.expires_at=-1 then Dify will never call this method again + + 2. For lease-based subscriptions (e.g., Microsoft Graph): + - Use the information in Subscription.properties to call service's lease renewal API if available + - Handle renewal limits (some services limit renewal count) + - Update the Subscription.properties and Subscription.expires_at for next time renewal if needed + + Args: + subscription: Current subscription with properties + credential_type: The type of the credentials, e.g., "api-key", "oauth2", "unauthorized" + credentials: Current authentication credentials from credentials_schema. + For API key auth, according to `credentials_schema` defined in the YAML. + For OAuth auth, according to `oauth_schema.credentials_schema` defined in the YAML. + For unauthorized auth, there is no credentials. + + Returns: + Subscription: Same subscription with extended expiration + or new properties and expires_at for next time renewal + + Raises: + SubscriptionError: For operational failures (API errors, invalid credentials) + """ + raise NotImplementedError("This plugin should implement `_refresh` method to enable subscription refresh") + + # OPTIONAL + def _fetch_parameter_options( + self, parameter: str, credentials: Mapping[str, Any], credential_type: CredentialType + ) -> list[ParameterOption]: + """ + Fetch the parameter options of the trigger. + + Implementation guidelines: + When you need to fetch parameter options from an external service, use the credentials + and credential_type to call the external service API, then return the options to Dify + for user selection. + + Args: + parameter: The parameter name for which to fetch options + credentials: Authentication credentials for the external service + credential_type: The type of credentials (e.g., "api-key", "oauth2", "unauthorized") + + Returns: + list[ParameterOption]: A list of available options for the parameter + + Examples: + GitHub Repositories: + >>> result = provider.fetch_parameter_options(parameter="repository") + >>> print(result) # [ParameterOption(label="owner/repo", value="owner/repo")] + + Slack Channels: + >>> result = provider.fetch_parameter_options(parameter="channel") + >>> print(result) +``` + +### Event + +```Python +class Event(ABC): + @abstractmethod + def _on_event(self, request: Request, parameters: Mapping[str, Any], payload: Mapping[str, Any]) -> Variables: + """ + Transform the incoming webhook request into structured Variables. + + This method should: + 1. Parse the webhook payload from the request + 2. Apply filtering logic based on parameters + 3. Extract relevant data matching the output_schema + 4. Return a structured Variables object + + Args: + request: The incoming webhook HTTP request containing the raw payload. + Use request.get_json() to parse JSON body. + parameters: User-configured parameters for filtering and transformation + (e.g., label filters, regex patterns, threshold values). + These come from the subscription configuration. + payload: The decoded payload from previous step `Trigger.dispatch_event`. + It will be delivered into `_on_event` method. + Returns: + Variables: Structured variables matching the output_schema + defined in the event's YAML configuration. + + Raises: + EventIgnoreError: When the event should be filtered out based on parameters + ValueError: When the payload is invalid or missing required fields + + Example: + >>> def _on_event(self, request, parameters): + ... payload = request.get_json() + ... + ... # Apply filters + ... if not self._matches_filters(payload, parameters): + ... raise EventIgnoreError() + ... + ... # Transform data + ... return Variables(variables={ + ... "title": payload["issue"]["title"], + ... "author": payload["issue"]["user"]["login"], + ... "url": payload["issue"]["html_url"], + ... }) + """ + + def _fetch_parameter_options(self, parameter: str) -> list[ParameterOption]: + """ + Fetch the parameter options of the trigger. + + To be implemented by subclasses. + + Also, it's optional to implement, that's why it's not an abstract method. + """ + raise NotImplementedError( + "This plugin should implement `_fetch_parameter_options` method to enable dynamic select parameter" + ) +``` + +{/* +Contributing Section +DO NOT edit this section! +It will be automatically generated by the script. +*/} + +--- + +[编辑此页面](https://github.com/langgenius/dify-docs/edit/main/plugin-dev-zh/0222-trigger-plugin.mdx) | [提交问题](https://github.com/langgenius/dify-docs/issues/new?template=docs.yml) + diff --git a/plugin-dev-zh/9231-extension-plugin.mdx b/plugin-dev-zh/9231-extension-plugin.mdx index 246b6f16..85207430 100644 --- a/plugin-dev-zh/9231-extension-plugin.mdx +++ b/plugin-dev-zh/9231-extension-plugin.mdx @@ -242,6 +242,8 @@ REMOTE_INSTALL_KEY=****-****-****-****-**** ![](https://assets-docs.dify.ai/2024/11/0fe19a8386b1234755395018bc2e0e35.png) +> 如果启动的时候遇到 An error occurred while parsing the data: b'handshake failed, invalid key',那就是 调试key 过期了,需要 前往“插件管理”页面重新获取调试 Key。 + 在插件内新增 Endpoint,随意填写名称和 `api_key` 等信息。访问自动生成的 URL,即可看到由插件提供的网页服务。 ![](https://assets-docs.dify.ai/2024/11/c76375b8df2449d0d8c31a7c2a337579.png) diff --git a/plugin-dev-zh/9241-bundle.mdx b/plugin-dev-zh/9241-bundle.mdx index a92f83a2..655add85 100644 --- a/plugin-dev-zh/9241-bundle.mdx +++ b/plugin-dev-zh/9241-bundle.mdx @@ -63,7 +63,7 @@ dify-plugin bundle append marketplace . --marketplace_pattern=langgenius/openai: 其中 marketplace\_pattern 为插件在 marketplace 中的引用,格式为 `组织名/插件名:版本号`。 -* **Github** +* **GitHub** 执行以下命令: