feat(trigger): add plugin trigger workflow support and refactor trigger system

- Add new workflow plugin trigger service for managing plugin-based triggers
- Implement trigger provider encryption utilities for secure credential storage
- Add custom trigger errors module for better error handling
- Refactor trigger provider and manager classes for improved plugin integration
- Update API endpoints to support plugin trigger workflows
- Add database migration for plugin trigger workflow support

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Harry
2025-09-04 12:47:51 +08:00
parent cc84a45244
commit a62d7aa3ee
16 changed files with 666 additions and 204 deletions

View File

@ -18,7 +18,7 @@ from models.workflow import AppTrigger, AppTriggerStatus, WorkflowWebhookTrigger
logger = logging.getLogger(__name__)
from models.workflow import WorkflowPluginTrigger
from services.workflow_plugin_trigger_service import WorkflowPluginTriggerService
class PluginTriggerApi(Resource):
@ -34,54 +34,21 @@ class PluginTriggerApi(Resource):
parser.add_argument("node_id", type=str, required=True, help="Node ID is required")
parser.add_argument("provider_id", type=str, required=True, help="Provider ID is required")
parser.add_argument("trigger_name", type=str, required=True, help="Trigger name is required")
parser.add_argument(
"triggered_by",
type=str,
required=False,
default="production",
choices=["debugger", "production"],
help="triggered_by must be debugger or production",
)
parser.add_argument("subscription_id", type=str, required=True, help="Subscription ID is required")
args = parser.parse_args()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
node_id = args["node_id"]
provider_id = args["provider_id"]
trigger_name = args["trigger_name"]
triggered_by = args["triggered_by"]
# Create trigger_id from provider_id and trigger_name
trigger_id = f"{provider_id}:{trigger_name}"
with Session(db.engine) as session:
# Check if plugin trigger already exists for this app, node, and environment
existing_trigger = session.scalar(
select(WorkflowPluginTrigger).where(
WorkflowPluginTrigger.app_id == app_model.id,
WorkflowPluginTrigger.node_id == node_id,
WorkflowPluginTrigger.triggered_by == triggered_by,
)
)
if existing_trigger:
raise BadRequest("Plugin trigger already exists for this node and environment")
# Create new plugin trigger
plugin_trigger = WorkflowPluginTrigger(
app_id=app_model.id,
node_id=node_id,
tenant_id=current_user.current_tenant_id,
provider_id=provider_id,
trigger_id=trigger_id,
triggered_by=triggered_by,
)
session.add(plugin_trigger)
session.commit()
session.refresh(plugin_trigger)
plugin_trigger = WorkflowPluginTriggerService.create_plugin_trigger(
app_id=app_model.id,
tenant_id=current_user.current_tenant_id,
node_id=args["node_id"],
provider_id=args["provider_id"],
trigger_name=args["trigger_name"],
subscription_id=args["subscription_id"],
)
return plugin_trigger
@ -93,33 +60,14 @@ class PluginTriggerApi(Resource):
"""Get plugin trigger"""
parser = reqparse.RequestParser()
parser.add_argument("node_id", type=str, required=True, help="Node ID is required")
parser.add_argument(
"triggered_by",
type=str,
required=False,
default="production",
choices=["debugger", "production"],
help="triggered_by must be debugger or production",
)
args = parser.parse_args()
node_id = args["node_id"]
triggered_by = args["triggered_by"]
plugin_trigger = WorkflowPluginTriggerService.get_plugin_trigger(
app_id=app_model.id,
node_id=args["node_id"],
)
with Session(db.engine) as session:
# Find plugin trigger
plugin_trigger = session.scalar(
select(WorkflowPluginTrigger).where(
WorkflowPluginTrigger.app_id == app_model.id,
WorkflowPluginTrigger.node_id == node_id,
WorkflowPluginTrigger.triggered_by == triggered_by,
WorkflowPluginTrigger.tenant_id == current_user.current_tenant_id,
)
)
if not plugin_trigger:
raise NotFound("Plugin trigger not found")
return plugin_trigger
return plugin_trigger
@setup_required
@login_required
@ -131,51 +79,22 @@ class PluginTriggerApi(Resource):
parser.add_argument("node_id", type=str, required=True, help="Node ID is required")
parser.add_argument("provider_id", type=str, required=False, help="Provider ID")
parser.add_argument("trigger_name", type=str, required=False, help="Trigger name")
parser.add_argument(
"triggered_by",
type=str,
required=False,
default="production",
choices=["debugger", "production"],
help="triggered_by must be debugger or production",
)
parser.add_argument("subscription_id", type=str, required=False, help="Subscription ID")
args = parser.parse_args()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
node_id = args["node_id"]
triggered_by = args["triggered_by"]
plugin_trigger = WorkflowPluginTriggerService.update_plugin_trigger(
app_id=app_model.id,
node_id=args["node_id"],
provider_id=args.get("provider_id"),
trigger_name=args.get("trigger_name"),
subscription_id=args.get("subscription_id"),
)
with Session(db.engine) as session:
# Find plugin trigger
plugin_trigger = session.scalar(
select(WorkflowPluginTrigger).where(
WorkflowPluginTrigger.app_id == app_model.id,
WorkflowPluginTrigger.node_id == node_id,
WorkflowPluginTrigger.triggered_by == triggered_by,
WorkflowPluginTrigger.tenant_id == current_user.current_tenant_id,
)
)
if not plugin_trigger:
raise NotFound("Plugin trigger not found")
# Update fields if provided
if args.get("provider_id"):
plugin_trigger.provider_id = args["provider_id"]
if args.get("trigger_name"):
# Update trigger_id if provider_id or trigger_name changed
provider_id = args.get("provider_id") or plugin_trigger.provider_id
trigger_name = args["trigger_name"]
plugin_trigger.trigger_id = f"{provider_id}:{trigger_name}"
session.commit()
session.refresh(plugin_trigger)
return plugin_trigger
return plugin_trigger
@setup_required
@login_required
@ -185,39 +104,16 @@ class PluginTriggerApi(Resource):
"""Delete plugin trigger"""
parser = reqparse.RequestParser()
parser.add_argument("node_id", type=str, required=True, help="Node ID is required")
parser.add_argument(
"triggered_by",
type=str,
required=False,
default="production",
choices=["debugger", "production"],
help="triggered_by must be debugger or production",
)
args = parser.parse_args()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
node_id = args["node_id"]
triggered_by = args["triggered_by"]
with Session(db.engine) as session:
# Find plugin trigger
plugin_trigger = session.scalar(
select(WorkflowPluginTrigger).where(
WorkflowPluginTrigger.app_id == app_model.id,
WorkflowPluginTrigger.node_id == node_id,
WorkflowPluginTrigger.triggered_by == triggered_by,
WorkflowPluginTrigger.tenant_id == current_user.current_tenant_id,
)
)
if not plugin_trigger:
raise NotFound("Plugin trigger not found")
session.delete(plugin_trigger)
session.commit()
WorkflowPluginTriggerService.delete_plugin_trigger(
app_id=app_model.id,
node_id=args["node_id"],
)
return {"result": "success"}, 204

View File

@ -117,6 +117,43 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
raise
class TriggerSubscriptionBuilderUpdateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self, provider, subscription_builder_id):
"""Update a subscription instance for a trigger provider"""
user = current_user
assert isinstance(user, Account)
assert user.current_tenant_id is not None
parser = reqparse.RequestParser()
# The name of the subscription builder
parser.add_argument("name", type=str, required=False, nullable=True, location="json")
# The parameters of the subscription builder
parser.add_argument("parameters", type=dict, required=False, nullable=True, location="json")
# The properties of the subscription builder
parser.add_argument("properties", type=dict, required=False, nullable=True, location="json")
# The credentials of the subscription builder
parser.add_argument("credentials", type=dict, required=False, nullable=True, location="json")
args = parser.parse_args()
try:
return jsonable_encoder(
TriggerSubscriptionBuilderService.update_trigger_subscription_builder(
tenant_id=user.current_tenant_id,
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
name=args.get("name", None),
parameters=args.get("parameters", None),
properties=args.get("properties", None),
credentials=args.get("credentials", None),
)
)
except Exception as e:
logger.exception("Error updating provider credential", exc_info=e)
raise
class TriggerSubscriptionBuilderBuildApi(Resource):
@setup_required
@login_required
@ -216,9 +253,26 @@ class TriggerOAuthAuthorizeApi(Resource):
redirect_uri=redirect_uri,
system_credentials=oauth_client_params,
)
# Create subscription builder
subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder(
tenant_id=tenant_id,
user_id=user.id,
provider_id=provider_id,
credentials={},
credential_type=CredentialType.OAUTH2,
credential_expires_at=0,
expires_at=0,
)
# Create response with cookie
response = make_response(jsonable_encoder(authorization_url_response))
response = make_response(
jsonable_encoder(
{
"authorization_url": authorization_url_response,
"subscription_builder": subscription_builder,
}
)
)
response.set_cookie(
"context_id",
context_id,
@ -410,6 +464,10 @@ api.add_resource(
TriggerSubscriptionBuilderCreateApi,
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/create",
)
api.add_resource(
TriggerSubscriptionBuilderUpdateApi,
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/update/<path:subscription_builder_id>",
)
api.add_resource(
TriggerSubscriptionBuilderVerifyApi,
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/verify/<path:subscription_builder_id>",