feat(trigger): introduce subscription builder and enhance trigger management

- Refactor trigger provider classes to improve naming consistency, including renaming classes for subscription management
- Implement new TriggerSubscriptionBuilderService for creating and verifying subscription builders
- Update API endpoints to support subscription builder creation and verification
- Enhance data models to include new attributes for subscription builders
- Remove the deprecated TriggerSubscriptionValidationService to streamline the codebase

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Harry
2025-09-02 12:06:27 +08:00
parent 694197a701
commit afd8989150
17 changed files with 544 additions and 476 deletions

View File

@ -210,12 +210,15 @@ class PluginTriggerProviderEntity(BaseModel):
class CredentialType(enum.StrEnum):
API_KEY = "api-key"
OAUTH2 = "oauth2"
UNAUTHORIZED = "unauthorized"
def get_name(self):
if self == CredentialType.API_KEY:
return "API KEY"
elif self == CredentialType.OAUTH2:
return "AUTH"
elif self == CredentialType.UNAUTHORIZED:
return "UNAUTHORIZED"
else:
return self.value.replace("-", " ").upper()
@ -236,5 +239,7 @@ class CredentialType(enum.StrEnum):
return cls.API_KEY
elif type_name == "oauth2":
return cls.OAUTH2
elif type_name == "unauthorized":
return cls.UNAUTHORIZED
else:
raise ValueError(f"Invalid credential type: {credential_type}")

View File

@ -248,14 +248,17 @@ class PluginTriggerDispatchResponse(BaseModel):
triggers: list[str]
raw_http_response: str
class TriggerSubscriptionResponse(BaseModel):
subscription: dict[str, Any]
class TriggerValidateProviderCredentialsResponse(BaseModel):
valid: bool
message: str
error: str
class TriggerDispatchResponse:
triggers: list[str]
response: Response

View File

@ -1,4 +1,5 @@
import binascii
from collections.abc import Mapping
from typing import Any
from flask import Request
@ -84,10 +85,10 @@ class PluginTriggerManager(BasePluginClient):
user_id: str,
provider: str,
trigger: str,
credentials: dict[str, Any],
credentials: Mapping[str, str],
credential_type: CredentialType,
request: Request,
parameters: dict[str, Any],
parameters: Mapping[str, Any],
) -> TriggerInvokeResponse:
"""
Invoke a trigger with the given parameters.
@ -121,7 +122,7 @@ class PluginTriggerManager(BasePluginClient):
raise ValueError("No response received from plugin daemon for invoke trigger")
def validate_provider_credentials(
self, tenant_id: str, user_id: str, provider: str, credentials: dict[str, Any]
self, tenant_id: str, user_id: str, provider: str, credentials: Mapping[str, str]
) -> TriggerValidateProviderCredentialsResponse:
"""
Validate the credentials of the trigger provider.
@ -155,7 +156,7 @@ class PluginTriggerManager(BasePluginClient):
tenant_id: str,
user_id: str,
provider: str,
subscription: dict[str, Any],
subscription: Mapping[str, Any],
request: Request,
) -> TriggerDispatchResponse:
"""
@ -194,9 +195,9 @@ class PluginTriggerManager(BasePluginClient):
tenant_id: str,
user_id: str,
provider: str,
credentials: dict[str, Any],
credentials: Mapping[str, str],
endpoint: str,
parameters: dict[str, Any],
parameters: Mapping[str, Any],
) -> TriggerSubscriptionResponse:
"""
Subscribe to a trigger.
@ -233,7 +234,7 @@ class PluginTriggerManager(BasePluginClient):
user_id: str,
provider: str,
subscription: Subscription,
credentials: dict[str, Any],
credentials: Mapping[str, str],
) -> TriggerSubscriptionResponse:
"""
Unsubscribe from a trigger.
@ -269,7 +270,7 @@ class PluginTriggerManager(BasePluginClient):
user_id: str,
provider: str,
subscription: Subscription,
credentials: dict[str, Any],
credentials: Mapping[str, str],
) -> TriggerSubscriptionResponse:
"""
Refresh a trigger subscription.

View File

@ -7,7 +7,6 @@ from core.entities.provider_entities import ProviderConfig
from core.plugin.entities.plugin_daemon import CredentialType
from core.trigger.entities.entities import (
OAuthSchema,
Subscription,
SubscriptionSchema,
TriggerDescription,
TriggerEntity,
@ -40,27 +39,4 @@ class TriggerApiEntity(BaseModel):
parameters: list[TriggerParameter] = Field(description="The parameters of the trigger")
output_schema: Optional[Mapping[str, Any]] = Field(description="The output schema of the trigger")
class SubscriptionValidation(BaseModel):
id: str
name: str
tenant_id: str
user_id: str
provider_id: str
endpoint: str
parameters: dict
properties: dict
credentials: dict
credential_type: str
credential_expires_at: int
expires_at: int
def to_subscription(self) -> Subscription:
return Subscription(
expires_at=self.expires_at,
endpoint=self.endpoint,
parameters=self.parameters,
properties=self.properties,
)
__all__ = ["TriggerApiEntity", "TriggerProviderApiEntity", "TriggerProviderSubscriptionApiEntity"]

View File

@ -115,6 +115,18 @@ class SubscriptionSchema(BaseModel):
description="The configuration schema stored in the subscription entity",
)
def get_default_parameters(self) -> Mapping[str, Any]:
"""Get the default parameters from the parameters schema"""
if not self.parameters_schema:
return {}
return {param.name: param.default for param in self.parameters_schema if param.default}
def get_default_properties(self) -> Mapping[str, Any]:
"""Get the default properties from the properties schema"""
if not self.properties_schema:
return {}
return {prop.name: prop.default for prop in self.properties_schema if prop.default}
class TriggerProviderEntity(BaseModel):
"""
@ -148,13 +160,7 @@ class Subscription(BaseModel):
)
endpoint: str = Field(..., description="The webhook endpoint URL allocated by Dify for receiving events")
parameters: dict[str, Any] | None = Field(
default=None,
description="""The parameters of the subscription, this is the creation parameters.
Only available when creating a new subscription by credentials(auto subscription), not manual subscription""",
)
properties: dict[str, Any] = Field(
properties: Mapping[str, Any] = Field(
..., description="Subscription data containing all properties and provider-specific information"
)
@ -177,10 +183,43 @@ class Unsubscription(BaseModel):
)
class RequestLog(BaseModel):
id: str
endpoint: str
request: dict
response: dict
created_at: str
class SubscriptionBuilder(BaseModel):
id: str
name: str | None = None
tenant_id: str
user_id: str
provider_id: str
endpoint_id: str
parameters: Mapping[str, Any]
properties: Mapping[str, Any]
credentials: Mapping[str, str]
credential_type: str | None = None
credential_expires_at: int | None = None
expires_at: int
def to_subscription(self) -> Subscription:
return Subscription(
expires_at=self.expires_at,
endpoint=self.endpoint_id,
parameters=self.parameters,
properties=self.properties,
)
# Export all entities
__all__ = [
"OAuthSchema",
"RequestLog",
"Subscription",
"SubscriptionBuilder",
"TriggerDescription",
"TriggerEntity",
"TriggerIdentity",

View File

@ -3,7 +3,8 @@ Trigger Provider Controller for managing trigger providers
"""
import logging
from typing import Optional
from collections.abc import Mapping
from typing import Any, Optional
from flask import Request
@ -20,6 +21,7 @@ from core.trigger.entities.api_entities import TriggerProviderApiEntity
from core.trigger.entities.entities import (
ProviderConfig,
Subscription,
SubscriptionSchema,
TriggerEntity,
TriggerProviderEntity,
TriggerProviderIdentity,
@ -91,18 +93,17 @@ class PluginTriggerProviderController:
return trigger
return None
def get_subscription_schema(self) -> list[ProviderConfig]:
def get_subscription_schema(self) -> SubscriptionSchema:
"""
Get subscription schema for this provider
:return: List of subscription config schemas
"""
# Return the parameters schema from the subscription schema
if self.entity.subscription_schema and self.entity.subscription_schema.parameters_schema:
return self.entity.subscription_schema.parameters_schema
return []
return self.entity.subscription_schema
def validate_credentials(self, credentials: dict) -> TriggerValidateProviderCredentialsResponse:
def validate_credentials(
self, user_id: str, credentials: Mapping[str, str]
) -> TriggerValidateProviderCredentialsResponse:
"""
Validate credentials against schema
@ -123,7 +124,7 @@ class PluginTriggerProviderController:
provider_id = self.get_provider_id()
return manager.validate_provider_credentials(
tenant_id=self.tenant_id,
user_id="system", # System validation
user_id=user_id,
provider=str(provider_id),
credentials=credentials,
)
@ -153,6 +154,7 @@ class PluginTriggerProviderController:
return self.entity.oauth_schema.credentials_schema.copy() if self.entity.oauth_schema else []
if credential_type == CredentialType.API_KEY:
return self.entity.credentials_schema.copy() if self.entity.credentials_schema else []
raise ValueError(f"Invalid credential type: {credential_type}")
def get_credential_schema_config(self, credential_type: CredentialType | str) -> list[BasicProviderConfig]:
"""
@ -168,7 +170,7 @@ class PluginTriggerProviderController:
"""
return self.entity.oauth_schema.client_schema.copy() if self.entity.oauth_schema else []
def dispatch(self,user_id: str, request: Request, subscription: Subscription) -> TriggerDispatchResponse:
def dispatch(self, user_id: str, request: Request, subscription: Subscription) -> TriggerDispatchResponse:
"""
Dispatch a trigger through plugin runtime
@ -193,8 +195,8 @@ class PluginTriggerProviderController:
self,
user_id: str,
trigger_name: str,
parameters: dict,
credentials: dict,
parameters: Mapping[str, Any],
credentials: Mapping[str, str],
credential_type: CredentialType,
request: Request,
) -> TriggerInvokeResponse:
@ -223,7 +225,9 @@ class PluginTriggerProviderController:
parameters=parameters,
)
def subscribe_trigger(self, user_id: str, endpoint: str, parameters: dict, credentials: dict) -> Subscription:
def subscribe_trigger(
self, user_id: str, endpoint: str, parameters: Mapping[str, Any], credentials: Mapping[str, str]
) -> Subscription:
"""
Subscribe to a trigger through plugin runtime
@ -247,7 +251,9 @@ class PluginTriggerProviderController:
return Subscription.model_validate(response.subscription)
def unsubscribe_trigger(self, user_id: str, subscription: Subscription, credentials: dict) -> Unsubscription:
def unsubscribe_trigger(
self, user_id: str, subscription: Subscription, credentials: Mapping[str, str]
) -> Unsubscription:
"""
Unsubscribe from a trigger through plugin runtime
@ -269,7 +275,7 @@ class PluginTriggerProviderController:
return Unsubscription.model_validate(response.subscription)
def refresh_trigger(self, subscription: Subscription, credentials: dict) -> Subscription:
def refresh_trigger(self, subscription: Subscription, credentials: Mapping[str, str]) -> Subscription:
"""
Refresh a trigger subscription through plugin runtime

View File

@ -3,7 +3,8 @@ Trigger Manager for loading and managing trigger providers and triggers
"""
import logging
from typing import Optional
from collections.abc import Mapping
from typing import Any, Optional
from flask import Request
@ -12,8 +13,8 @@ from core.plugin.entities.plugin_daemon import CredentialType
from core.plugin.entities.request import TriggerInvokeResponse
from core.plugin.impl.trigger import PluginTriggerManager
from core.trigger.entities.entities import (
ProviderConfig,
Subscription,
SubscriptionSchema,
TriggerEntity,
Unsubscription,
)
@ -116,19 +117,20 @@ class TriggerManager:
@classmethod
def validate_trigger_credentials(
cls, tenant_id: str, provider_id: TriggerProviderID, credentials: dict
cls, tenant_id: str, provider_id: TriggerProviderID, user_id: str, credentials: Mapping[str, str]
) -> tuple[bool, str]:
"""
Validate trigger provider credentials
:param tenant_id: Tenant ID
:param provider_id: Provider ID
:param user_id: User ID
:param credentials: Credentials to validate
:return: Tuple of (is_valid, error_message)
"""
try:
provider = cls.get_trigger_provider(tenant_id, provider_id)
validation_result = provider.validate_credentials(credentials)
validation_result = provider.validate_credentials(user_id, credentials)
return validation_result.valid, validation_result.message if not validation_result.valid else ""
except Exception as e:
return False, str(e)
@ -140,8 +142,8 @@ class TriggerManager:
user_id: str,
provider_id: TriggerProviderID,
trigger_name: str,
parameters: dict,
credentials: dict,
parameters: Mapping[str, Any],
credentials: Mapping[str, str],
credential_type: CredentialType,
request: Request,
) -> TriggerInvokeResponse:
@ -171,8 +173,8 @@ class TriggerManager:
user_id: str,
provider_id: TriggerProviderID,
endpoint: str,
parameters: dict,
credentials: dict,
parameters: Mapping[str, Any],
credentials: Mapping[str, str],
) -> Subscription:
"""
Subscribe to a trigger (e.g., register webhook)
@ -197,7 +199,7 @@ class TriggerManager:
user_id: str,
provider_id: TriggerProviderID,
subscription: Subscription,
credentials: dict,
credentials: Mapping[str, str],
) -> Unsubscription:
"""
Unsubscribe from a trigger
@ -213,7 +215,7 @@ class TriggerManager:
return provider.unsubscribe_trigger(user_id=user_id, subscription=subscription, credentials=credentials)
@classmethod
def get_provider_subscription_schema(cls, tenant_id: str, provider_id: TriggerProviderID) -> list[ProviderConfig]:
def get_provider_subscription_schema(cls, tenant_id: str, provider_id: TriggerProviderID) -> SubscriptionSchema:
"""
Get provider subscription schema
@ -228,9 +230,8 @@ class TriggerManager:
cls,
tenant_id: str,
provider_id: TriggerProviderID,
trigger_name: str,
subscription: Subscription,
credentials: dict,
credentials: Mapping[str, str],
) -> Subscription:
"""
Refresh a trigger subscription
@ -242,7 +243,7 @@ class TriggerManager:
:param credentials: Provider credentials
:return: Refreshed subscription result
"""
return cls.get_trigger_provider(tenant_id, provider_id).refresh_trigger(trigger_name, subscription, credentials)
return cls.get_trigger_provider(tenant_id, provider_id).refresh_trigger(subscription, credentials)
# Export

View File

@ -0,0 +1,5 @@
from configs import dify_config
def parse_endpoint_id(endpoint_id: str) -> str:
return f"{dify_config.CONSOLE_API_URL}/console/api/trigger/endpoint/{endpoint_id}"