From cac60a25bb6bf65c6b874997b08ec213655fd7bd Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Thu, 30 Oct 2025 15:27:02 +0800 Subject: [PATCH 01/17] cleanup: migrations --- ...-4558cfabe44e_add_workflow_trigger_logs.py | 67 ----- ...5871f634954d_add_workflow_webhook_table.py | 47 ---- ...203-9ee7d347f4c1_add_app_triggers_table.py | 47 ---- ...c19938f630b6_add_workflow_schedule_plan.py | 47 ---- ..._10_27_1205-132392a2635f_plugin_trigger.py | 102 -------- ..._1752-5ed4b21dbb8d_trigger_log_metadata.py | 32 --- ..._30_1518-669ffd70119c_introduce_trigger.py | 235 ++++++++++++++++++ api/models/trigger.py | 2 +- 8 files changed, 236 insertions(+), 343 deletions(-) delete mode 100644 api/migrations/versions/2025_10_27_1201-4558cfabe44e_add_workflow_trigger_logs.py delete mode 100644 api/migrations/versions/2025_10_27_1202-5871f634954d_add_workflow_webhook_table.py delete mode 100644 api/migrations/versions/2025_10_27_1203-9ee7d347f4c1_add_app_triggers_table.py delete mode 100644 api/migrations/versions/2025_10_27_1204-c19938f630b6_add_workflow_schedule_plan.py delete mode 100644 api/migrations/versions/2025_10_27_1205-132392a2635f_plugin_trigger.py delete mode 100644 api/migrations/versions/2025_10_27_1752-5ed4b21dbb8d_trigger_log_metadata.py create mode 100644 api/migrations/versions/2025_10_30_1518-669ffd70119c_introduce_trigger.py diff --git a/api/migrations/versions/2025_10_27_1201-4558cfabe44e_add_workflow_trigger_logs.py b/api/migrations/versions/2025_10_27_1201-4558cfabe44e_add_workflow_trigger_logs.py deleted file mode 100644 index 1fe46972c1..0000000000 --- a/api/migrations/versions/2025_10_27_1201-4558cfabe44e_add_workflow_trigger_logs.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Add workflow trigger logs table - -Revision ID: 4558cfabe44e -Revises: ae662b25d9bc -Create Date: 2025-10-27 12:01:00.000000 - -""" -from alembic import op -import models as models -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '4558cfabe44e' -down_revision = '03f8dcbc611e' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('workflow_trigger_logs', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('app_id', models.types.StringUUID(), nullable=False), - sa.Column('workflow_id', models.types.StringUUID(), nullable=False), - sa.Column('workflow_run_id', models.types.StringUUID(), nullable=True), - sa.Column('root_node_id', sa.String(length=255), nullable=True), - sa.Column('trigger_type', sa.String(length=50), nullable=False), - sa.Column('trigger_data', sa.Text(), nullable=False), - sa.Column('inputs', sa.Text(), nullable=False), - sa.Column('outputs', sa.Text(), nullable=True), - sa.Column('status', sa.String(length=50), nullable=False), - sa.Column('error', sa.Text(), nullable=True), - sa.Column('queue_name', sa.String(length=100), nullable=False), - sa.Column('celery_task_id', sa.String(length=255), nullable=True), - sa.Column('retry_count', sa.Integer(), nullable=False), - sa.Column('elapsed_time', sa.Float(), nullable=True), - sa.Column('total_tokens', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('created_by_role', sa.String(length=255), nullable=False), - sa.Column('created_by', sa.String(length=255), nullable=False), - sa.Column('triggered_at', sa.DateTime(), nullable=True), - sa.Column('finished_at', sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint('id', name='workflow_trigger_log_pkey') - ) - with op.batch_alter_table('workflow_trigger_logs', schema=None) as batch_op: - batch_op.create_index('workflow_trigger_log_created_at_idx', ['created_at'], unique=False) - batch_op.create_index('workflow_trigger_log_status_idx', ['status'], unique=False) - batch_op.create_index('workflow_trigger_log_tenant_app_idx', ['tenant_id', 'app_id'], unique=False) - batch_op.create_index('workflow_trigger_log_workflow_id_idx', ['workflow_id'], unique=False) - batch_op.create_index('workflow_trigger_log_workflow_run_idx', ['workflow_run_id'], unique=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('workflow_trigger_logs', schema=None) as batch_op: - batch_op.drop_index('workflow_trigger_log_workflow_run_idx') - batch_op.drop_index('workflow_trigger_log_workflow_id_idx') - batch_op.drop_index('workflow_trigger_log_tenant_app_idx') - batch_op.drop_index('workflow_trigger_log_status_idx') - batch_op.drop_index('workflow_trigger_log_created_at_idx') - - op.drop_table('workflow_trigger_logs') - # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_10_27_1202-5871f634954d_add_workflow_webhook_table.py b/api/migrations/versions/2025_10_27_1202-5871f634954d_add_workflow_webhook_table.py deleted file mode 100644 index 43466a0697..0000000000 --- a/api/migrations/versions/2025_10_27_1202-5871f634954d_add_workflow_webhook_table.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Add workflow webhook table - -Revision ID: 5871f634954d -Revises: 4558cfabe44e -Create Date: 2025-10-27 12:02:00.000000 - -""" -from alembic import op -import models as models -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '5871f634954d' -down_revision = '4558cfabe44e' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('workflow_webhook_triggers', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), - sa.Column('app_id', models.types.StringUUID(), nullable=False), - sa.Column('node_id', sa.String(length=64), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('webhook_id', sa.String(length=24), nullable=False), - sa.Column('created_by', models.types.StringUUID(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id', name='workflow_webhook_trigger_pkey'), - sa.UniqueConstraint('app_id', 'node_id', name='uniq_node'), - sa.UniqueConstraint('webhook_id', name='uniq_webhook_id') - ) - with op.batch_alter_table('workflow_webhook_triggers', schema=None) as batch_op: - batch_op.create_index('workflow_webhook_trigger_tenant_idx', ['tenant_id'], unique=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('workflow_webhook_triggers', schema=None) as batch_op: - batch_op.drop_index('workflow_webhook_trigger_tenant_idx') - - op.drop_table('workflow_webhook_triggers') - # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_10_27_1203-9ee7d347f4c1_add_app_triggers_table.py b/api/migrations/versions/2025_10_27_1203-9ee7d347f4c1_add_app_triggers_table.py deleted file mode 100644 index fe4cd24ad6..0000000000 --- a/api/migrations/versions/2025_10_27_1203-9ee7d347f4c1_add_app_triggers_table.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Add app triggers table - -Revision ID: 9ee7d347f4c1 -Revises: 5871f634954d -Create Date: 2025-10-27 12:03:00.000000 - -""" -from alembic import op -import models as models -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '9ee7d347f4c1' -down_revision = '5871f634954d' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('app_triggers', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('app_id', models.types.StringUUID(), nullable=False), - sa.Column('node_id', sa.String(length=64), nullable=False), - sa.Column('trigger_type', sa.String(length=50), nullable=False), - sa.Column('title', sa.String(length=255), nullable=False), - sa.Column('provider_name', sa.String(length=255), server_default='', nullable=True), - sa.Column('status', sa.String(length=50), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id', name='app_trigger_pkey') - ) - with op.batch_alter_table('app_triggers', schema=None) as batch_op: - batch_op.create_index('app_trigger_tenant_app_idx', ['tenant_id', 'app_id'], unique=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('app_triggers', schema=None) as batch_op: - batch_op.drop_index('app_trigger_tenant_app_idx') - - op.drop_table('app_triggers') - # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_10_27_1204-c19938f630b6_add_workflow_schedule_plan.py b/api/migrations/versions/2025_10_27_1204-c19938f630b6_add_workflow_schedule_plan.py deleted file mode 100644 index 85e7e0c735..0000000000 --- a/api/migrations/versions/2025_10_27_1204-c19938f630b6_add_workflow_schedule_plan.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Add workflow schedule plan table - -Revision ID: c19938f630b6 -Revises: 9ee7d347f4c1 -Create Date: 2025-10-27 12:04:00.000000 - -""" -from alembic import op -import models as models -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'c19938f630b6' -down_revision = '9ee7d347f4c1' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('workflow_schedule_plans', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), - sa.Column('app_id', models.types.StringUUID(), nullable=False), - sa.Column('node_id', sa.String(length=64), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('cron_expression', sa.String(length=255), nullable=False), - sa.Column('timezone', sa.String(length=64), nullable=False), - sa.Column('next_run_at', sa.DateTime(), nullable=True), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id', name='workflow_schedule_plan_pkey'), - sa.UniqueConstraint('app_id', 'node_id', name='uniq_app_node') - ) - with op.batch_alter_table('workflow_schedule_plans', schema=None) as batch_op: - batch_op.create_index('workflow_schedule_plan_next_idx', ['next_run_at'], unique=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('workflow_schedule_plans', schema=None) as batch_op: - batch_op.drop_index('workflow_schedule_plan_next_idx') - - op.drop_table('workflow_schedule_plans') - # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_10_27_1205-132392a2635f_plugin_trigger.py b/api/migrations/versions/2025_10_27_1205-132392a2635f_plugin_trigger.py deleted file mode 100644 index 426be1b071..0000000000 --- a/api/migrations/versions/2025_10_27_1205-132392a2635f_plugin_trigger.py +++ /dev/null @@ -1,102 +0,0 @@ -"""plugin_trigger - -Revision ID: 132392a2635f -Revises: c19938f630b6 -Create Date: 2025-10-27 12:05:00.000000 - -""" -from alembic import op -import models as models -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '132392a2635f' -down_revision = 'c19938f630b6' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('trigger_oauth_system_clients', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), - sa.Column('plugin_id', sa.String(length=512), nullable=False), - sa.Column('provider', sa.String(length=255), nullable=False), - sa.Column('encrypted_oauth_params', sa.Text(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id', name='trigger_oauth_system_client_pkey'), - sa.UniqueConstraint('plugin_id', 'provider', name='trigger_oauth_system_client_plugin_id_provider_idx') - ) - op.create_table('trigger_oauth_tenant_clients', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('plugin_id', sa.String(length=512), nullable=False), - sa.Column('provider', sa.String(length=255), nullable=False), - sa.Column('enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False), - sa.Column('encrypted_oauth_params', sa.Text(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id', name='trigger_oauth_tenant_client_pkey'), - sa.UniqueConstraint('tenant_id', 'plugin_id', 'provider', name='unique_trigger_oauth_tenant_client') - ) - op.create_table('trigger_subscriptions', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False, comment='Subscription instance name'), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('user_id', models.types.StringUUID(), nullable=False), - sa.Column('provider_id', sa.String(length=255), nullable=False, comment='Provider identifier (e.g., plugin_id/provider_name)'), - sa.Column('endpoint_id', sa.String(length=255), nullable=False, comment='Subscription endpoint'), - sa.Column('parameters', sa.JSON(), nullable=False, comment='Subscription parameters JSON'), - sa.Column('properties', sa.JSON(), nullable=False, comment='Subscription properties JSON'), - sa.Column('credentials', sa.JSON(), nullable=False, comment='Subscription credentials JSON'), - sa.Column('credential_type', sa.String(length=50), nullable=False, comment='oauth or api_key'), - sa.Column('credential_expires_at', sa.Integer(), nullable=False, comment='OAuth token expiration timestamp, -1 for never'), - sa.Column('expires_at', sa.Integer(), nullable=False, comment='Subscription instance expiration timestamp, -1 for never'), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id', name='trigger_provider_pkey'), - sa.UniqueConstraint('tenant_id', 'provider_id', 'name', name='unique_trigger_provider') - ) - with op.batch_alter_table('trigger_subscriptions', schema=None) as batch_op: - batch_op.create_index('idx_trigger_providers_endpoint', ['endpoint_id'], unique=True) - batch_op.create_index('idx_trigger_providers_tenant_endpoint', ['tenant_id', 'endpoint_id'], unique=False) - batch_op.create_index('idx_trigger_providers_tenant_provider', ['tenant_id', 'provider_id'], unique=False) - - # Create workflow_plugin_triggers table with final schema (merged from all 4 migrations) - op.create_table('workflow_plugin_triggers', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), - sa.Column('app_id', models.types.StringUUID(), nullable=False), - sa.Column('node_id', sa.String(length=64), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('provider_id', sa.String(length=512), nullable=False), - sa.Column('subscription_id', sa.String(length=255), nullable=False), - sa.Column('event_name', sa.String(length=255), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id', name='workflow_plugin_trigger_pkey'), - sa.UniqueConstraint('app_id', 'node_id', name='uniq_app_node_subscription') - ) - with op.batch_alter_table('workflow_plugin_triggers', schema=None) as batch_op: - batch_op.create_index('workflow_plugin_trigger_tenant_subscription_idx', ['tenant_id', 'subscription_id', 'event_name'], unique=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('workflow_plugin_triggers', schema=None) as batch_op: - batch_op.drop_index('workflow_plugin_trigger_tenant_subscription_idx') - - op.drop_table('workflow_plugin_triggers') - with op.batch_alter_table('trigger_subscriptions', schema=None) as batch_op: - batch_op.drop_index('idx_trigger_providers_tenant_provider') - batch_op.drop_index('idx_trigger_providers_tenant_endpoint') - batch_op.drop_index('idx_trigger_providers_endpoint') - - op.drop_table('trigger_subscriptions') - op.drop_table('trigger_oauth_tenant_clients') - op.drop_table('trigger_oauth_system_clients') - - # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_10_27_1752-5ed4b21dbb8d_trigger_log_metadata.py b/api/migrations/versions/2025_10_27_1752-5ed4b21dbb8d_trigger_log_metadata.py deleted file mode 100644 index 089246d2fa..0000000000 --- a/api/migrations/versions/2025_10_27_1752-5ed4b21dbb8d_trigger_log_metadata.py +++ /dev/null @@ -1,32 +0,0 @@ -"""trigger_log_metadata - -Revision ID: 5ed4b21dbb8d -Revises: 132392a2635f -Create Date: 2025-10-27 17:52:35.658975 - -""" -from alembic import op -import models as models -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '5ed4b21dbb8d' -down_revision = '132392a2635f' -branch_labels = None -depends_on = None - - -def upgrade(): - with op.batch_alter_table('workflow_trigger_logs', schema=None) as batch_op: - batch_op.add_column(sa.Column('trigger_metadata', sa.Text(), nullable=True)) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('workflow_trigger_logs', schema=None) as batch_op: - batch_op.drop_column('trigger_metadata') - - # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_10_30_1518-669ffd70119c_introduce_trigger.py b/api/migrations/versions/2025_10_30_1518-669ffd70119c_introduce_trigger.py new file mode 100644 index 0000000000..c03d64b234 --- /dev/null +++ b/api/migrations/versions/2025_10_30_1518-669ffd70119c_introduce_trigger.py @@ -0,0 +1,235 @@ +"""introduce_trigger + +Revision ID: 669ffd70119c +Revises: 03f8dcbc611e +Create Date: 2025-10-30 15:18:49.549156 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + +from models.enums import AppTriggerStatus, AppTriggerType + + +# revision identifiers, used by Alembic. +revision = '669ffd70119c' +down_revision = '03f8dcbc611e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('app_triggers', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('node_id', sa.String(length=64), nullable=False), + sa.Column('trigger_type', models.types.EnumText(AppTriggerType, length=50), nullable=False), + sa.Column('title', sa.String(length=255), nullable=False), + sa.Column('provider_name', sa.String(length=255), server_default='', nullable=True), + sa.Column('status', models.types.EnumText(AppTriggerStatus, length=50), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id', name='app_trigger_pkey') + ) + with op.batch_alter_table('app_triggers', schema=None) as batch_op: + batch_op.create_index('app_trigger_tenant_app_idx', ['tenant_id', 'app_id'], unique=False) + + op.create_table('trigger_oauth_system_clients', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('plugin_id', sa.String(length=512), nullable=False), + sa.Column('provider', sa.String(length=255), nullable=False), + sa.Column('encrypted_oauth_params', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='trigger_oauth_system_client_pkey'), + sa.UniqueConstraint('plugin_id', 'provider', name='trigger_oauth_system_client_plugin_id_provider_idx') + ) + op.create_table('trigger_oauth_tenant_clients', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('plugin_id', sa.String(length=512), nullable=False), + sa.Column('provider', sa.String(length=255), nullable=False), + sa.Column('enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False), + sa.Column('encrypted_oauth_params', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='trigger_oauth_tenant_client_pkey'), + sa.UniqueConstraint('tenant_id', 'plugin_id', 'provider', name='unique_trigger_oauth_tenant_client') + ) + op.create_table('trigger_subscriptions', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False, comment='Subscription instance name'), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('user_id', models.types.StringUUID(), nullable=False), + sa.Column('provider_id', sa.String(length=255), nullable=False, comment='Provider identifier (e.g., plugin_id/provider_name)'), + sa.Column('endpoint_id', sa.String(length=255), nullable=False, comment='Subscription endpoint'), + sa.Column('parameters', sa.JSON(), nullable=False, comment='Subscription parameters JSON'), + sa.Column('properties', sa.JSON(), nullable=False, comment='Subscription properties JSON'), + sa.Column('credentials', sa.JSON(), nullable=False, comment='Subscription credentials JSON'), + sa.Column('credential_type', sa.String(length=50), nullable=False, comment='oauth or api_key'), + sa.Column('credential_expires_at', sa.Integer(), nullable=False, comment='OAuth token expiration timestamp, -1 for never'), + sa.Column('expires_at', sa.Integer(), nullable=False, comment='Subscription instance expiration timestamp, -1 for never'), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='trigger_provider_pkey'), + sa.UniqueConstraint('tenant_id', 'provider_id', 'name', name='unique_trigger_provider') + ) + with op.batch_alter_table('trigger_subscriptions', schema=None) as batch_op: + batch_op.create_index('idx_trigger_providers_endpoint', ['endpoint_id'], unique=True) + batch_op.create_index('idx_trigger_providers_tenant_endpoint', ['tenant_id', 'endpoint_id'], unique=False) + batch_op.create_index('idx_trigger_providers_tenant_provider', ['tenant_id', 'provider_id'], unique=False) + + op.create_table('workflow_plugin_triggers', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('node_id', sa.String(length=64), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('provider_id', sa.String(length=512), nullable=False), + sa.Column('event_name', sa.String(length=255), nullable=False), + sa.Column('subscription_id', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='workflow_plugin_trigger_pkey'), + sa.UniqueConstraint('app_id', 'node_id', name='uniq_app_node_subscription') + ) + with op.batch_alter_table('workflow_plugin_triggers', schema=None) as batch_op: + batch_op.create_index('workflow_plugin_trigger_tenant_subscription_idx', ['tenant_id', 'subscription_id', 'event_name'], unique=False) + + op.create_table('workflow_schedule_plans', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('node_id', sa.String(length=64), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('cron_expression', sa.String(length=255), nullable=False), + sa.Column('timezone', sa.String(length=64), nullable=False), + sa.Column('next_run_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='workflow_schedule_plan_pkey'), + sa.UniqueConstraint('app_id', 'node_id', name='uniq_app_node') + ) + with op.batch_alter_table('workflow_schedule_plans', schema=None) as batch_op: + batch_op.create_index('workflow_schedule_plan_next_idx', ['next_run_at'], unique=False) + + op.create_table('workflow_trigger_logs', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('workflow_id', models.types.StringUUID(), nullable=False), + sa.Column('workflow_run_id', models.types.StringUUID(), nullable=True), + sa.Column('root_node_id', sa.String(length=255), nullable=True), + sa.Column('trigger_metadata', sa.Text(), nullable=False), + sa.Column('trigger_type', models.types.EnumText(AppTriggerType, length=50), nullable=False), + sa.Column('trigger_data', sa.Text(), nullable=False), + sa.Column('inputs', sa.Text(), nullable=False), + sa.Column('outputs', sa.Text(), nullable=True), + sa.Column('status', models.types.EnumText(AppTriggerStatus, length=50), nullable=False), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('queue_name', sa.String(length=100), nullable=False), + sa.Column('celery_task_id', sa.String(length=255), nullable=True), + sa.Column('retry_count', sa.Integer(), nullable=False), + sa.Column('elapsed_time', sa.Float(), nullable=True), + sa.Column('total_tokens', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('created_by_role', sa.String(length=255), nullable=False), + sa.Column('created_by', sa.String(length=255), nullable=False), + sa.Column('triggered_at', sa.DateTime(), nullable=True), + sa.Column('finished_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id', name='workflow_trigger_log_pkey') + ) + with op.batch_alter_table('workflow_trigger_logs', schema=None) as batch_op: + batch_op.create_index('workflow_trigger_log_created_at_idx', ['created_at'], unique=False) + batch_op.create_index('workflow_trigger_log_status_idx', ['status'], unique=False) + batch_op.create_index('workflow_trigger_log_tenant_app_idx', ['tenant_id', 'app_id'], unique=False) + batch_op.create_index('workflow_trigger_log_workflow_id_idx', ['workflow_id'], unique=False) + batch_op.create_index('workflow_trigger_log_workflow_run_idx', ['workflow_run_id'], unique=False) + + op.create_table('workflow_webhook_triggers', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('node_id', sa.String(length=64), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('webhook_id', sa.String(length=24), nullable=False), + sa.Column('created_by', models.types.StringUUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='workflow_webhook_trigger_pkey'), + sa.UniqueConstraint('app_id', 'node_id', name='uniq_node'), + sa.UniqueConstraint('webhook_id', name='uniq_webhook_id') + ) + with op.batch_alter_table('workflow_webhook_triggers', schema=None) as batch_op: + batch_op.create_index('workflow_webhook_trigger_tenant_idx', ['tenant_id'], unique=False) + + with op.batch_alter_table('celery_taskmeta', schema=None) as batch_op: + batch_op.alter_column('task_id', + existing_type=sa.VARCHAR(length=155), + nullable=False) + batch_op.alter_column('status', + existing_type=sa.VARCHAR(length=50), + nullable=False) + + with op.batch_alter_table('celery_tasksetmeta', schema=None) as batch_op: + batch_op.alter_column('taskset_id', + existing_type=sa.VARCHAR(length=155), + nullable=False) + + with op.batch_alter_table('providers', schema=None) as batch_op: + batch_op.drop_column('credential_status') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('providers', schema=None) as batch_op: + batch_op.add_column(sa.Column('credential_status', sa.VARCHAR(length=20), server_default=sa.text("'active'::character varying"), autoincrement=False, nullable=True)) + + with op.batch_alter_table('celery_tasksetmeta', schema=None) as batch_op: + batch_op.alter_column('taskset_id', + existing_type=sa.VARCHAR(length=155), + nullable=True) + + with op.batch_alter_table('celery_taskmeta', schema=None) as batch_op: + batch_op.alter_column('status', + existing_type=sa.VARCHAR(length=50), + nullable=True) + batch_op.alter_column('task_id', + existing_type=sa.VARCHAR(length=155), + nullable=True) + + with op.batch_alter_table('workflow_webhook_triggers', schema=None) as batch_op: + batch_op.drop_index('workflow_webhook_trigger_tenant_idx') + + op.drop_table('workflow_webhook_triggers') + with op.batch_alter_table('workflow_trigger_logs', schema=None) as batch_op: + batch_op.drop_index('workflow_trigger_log_workflow_run_idx') + batch_op.drop_index('workflow_trigger_log_workflow_id_idx') + batch_op.drop_index('workflow_trigger_log_tenant_app_idx') + batch_op.drop_index('workflow_trigger_log_status_idx') + batch_op.drop_index('workflow_trigger_log_created_at_idx') + + op.drop_table('workflow_trigger_logs') + with op.batch_alter_table('workflow_schedule_plans', schema=None) as batch_op: + batch_op.drop_index('workflow_schedule_plan_next_idx') + + op.drop_table('workflow_schedule_plans') + with op.batch_alter_table('workflow_plugin_triggers', schema=None) as batch_op: + batch_op.drop_index('workflow_plugin_trigger_tenant_subscription_idx') + + op.drop_table('workflow_plugin_triggers') + with op.batch_alter_table('trigger_subscriptions', schema=None) as batch_op: + batch_op.drop_index('idx_trigger_providers_tenant_provider') + batch_op.drop_index('idx_trigger_providers_tenant_endpoint') + batch_op.drop_index('idx_trigger_providers_endpoint') + + op.drop_table('trigger_subscriptions') + op.drop_table('trigger_oauth_tenant_clients') + op.drop_table('trigger_oauth_system_clients') + with op.batch_alter_table('app_triggers', schema=None) as batch_op: + batch_op.drop_index('app_trigger_tenant_app_idx') + + op.drop_table('app_triggers') + # ### end Alembic commands ### diff --git a/api/models/trigger.py b/api/models/trigger.py index 5237a512e4..22bdcbca33 100644 --- a/api/models/trigger.py +++ b/api/models/trigger.py @@ -196,7 +196,7 @@ class WorkflowTriggerLog(Base): workflow_id: Mapped[str] = mapped_column(StringUUID, nullable=False) workflow_run_id: Mapped[Optional[str]] = mapped_column(StringUUID, nullable=True) root_node_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) - trigger_metadata: Mapped[Optional[str]] = mapped_column(sa.Text, nullable=True) + trigger_metadata: Mapped[str] = mapped_column(sa.Text, nullable=False) trigger_type: Mapped[str] = mapped_column(EnumText(AppTriggerType, length=50), nullable=False) trigger_data: Mapped[str] = mapped_column(sa.Text, nullable=False) # Full TriggerData as JSON inputs: Mapped[str] = mapped_column(sa.Text, nullable=False) # Just inputs for easy viewing From 1d03e0e9fc6507275b87da42540909c61d78b76a Mon Sep 17 00:00:00 2001 From: yessenia Date: Wed, 29 Oct 2025 18:51:27 +0800 Subject: [PATCH 02/17] fix(trigger): hide input params when no subscription --- .../components/plugins/readme-panel/index.tsx | 50 ++++---- .../_base/components/workflow-panel/index.tsx | 114 ++++++++++-------- .../workflow-panel/trigger-subscription.tsx | 33 +---- .../components/trigger-form/index.tsx | 6 +- .../components/trigger-form/item.tsx | 6 +- .../workflow/nodes/trigger-plugin/panel.tsx | 7 +- .../nodes/trigger-plugin/use-config.ts | 20 ++- 7 files changed, 111 insertions(+), 125 deletions(-) diff --git a/web/app/components/plugins/readme-panel/index.tsx b/web/app/components/plugins/readme-panel/index.tsx index b77d59fb0b..70d1e0db2c 100644 --- a/web/app/components/plugins/readme-panel/index.tsx +++ b/web/app/components/plugins/readme-panel/index.tsx @@ -2,7 +2,6 @@ import ActionButton from '@/app/components/base/action-button' import Loading from '@/app/components/base/loading' import { Markdown } from '@/app/components/base/markdown' -import Modal from '@/app/components/base/modal' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { usePluginReadme } from '@/service/use-plugins' import cn from '@/utils/classnames' @@ -85,29 +84,36 @@ const ReadmePanel: FC = () => { ) - return showType === ReadmeShowType.drawer ? createPortal( -
-
- {children} + const portalContent = showType === ReadmeShowType.drawer + ? ( +
+
+ {children} +
-
, + ) + : ( +
+
{ + event.stopPropagation() + }} + > + {children} +
+
+ ) + + return createPortal( + portalContent, document.body, - ) : ( - - {children} - ) } diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 32b9cb2671..93589795a6 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -1,5 +1,17 @@ import { useStore as useAppStore } from '@/app/components/app/store' +import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' import Tooltip from '@/app/components/base/tooltip' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { + AuthCategory, + AuthorizedInDataSourceNode, + AuthorizedInNode, + PluginAuth, + PluginAuthInDataSourceNode, +} from '@/app/components/plugins/plugin-auth' +import { usePluginStore } from '@/app/components/plugins/plugin-detail-panel/store' +import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list' +import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' import BlockIcon from '@/app/components/workflow/block-icon' import { WorkflowHistoryEvent, @@ -11,7 +23,14 @@ import { useToolIcon, useWorkflowHistory, } from '@/app/components/workflow/hooks' +import { useHooksStore } from '@/app/components/workflow/hooks-store' +import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud' import Split from '@/app/components/workflow/nodes/_base/components/split' +import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form' +import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types' +import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types' +import { useLogs } from '@/app/components/workflow/run/hooks' +import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' import { useStore } from '@/app/components/workflow/store' import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types' import { @@ -20,16 +39,18 @@ import { hasRetryNode, isSupportCustomRunForm, } from '@/app/components/workflow/utils' +import { useModalContext } from '@/context/modal-context' +import { useAllBuiltInTools } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' +import { FlowType } from '@/types/common' +import { canFindTool } from '@/utils' import cn from '@/utils/classnames' import { RiCloseLine, RiPlayLargeLine, } from '@remixicon/react' -import type { - FC, - ReactNode, -} from 'react' +import { debounce } from 'lodash-es' +import type { FC, ReactNode } from 'react' import React, { cloneElement, memo, @@ -42,44 +63,18 @@ import React, { import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' import { useResizePanel } from '../../hooks/use-resize-panel' +import BeforeRunForm from '../before-run-form' +import PanelWrap from '../before-run-form/panel-wrap' import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel' import HelpLink from '../help-link' import NextStep from '../next-step' import PanelOperator from '../panel-operator' import RetryOnPanel from '../retry/retry-on-panel' -import { - DescriptionInput, - TitleInput, -} from '../title-description-input' -import Tab, { TabType } from './tab' -// import AuthMethodSelector from '@/app/components/workflow/nodes/trigger-plugin/components/auth-method-selector' -import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' -import { - AuthCategory, - AuthorizedInDataSourceNode, - AuthorizedInNode, - PluginAuth, - PluginAuthInDataSourceNode, -} from '@/app/components/plugins/plugin-auth' -import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list' -import { useHooksStore } from '@/app/components/workflow/hooks-store' -import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud' -import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form' -import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types' -import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types' -import { useLogs } from '@/app/components/workflow/run/hooks' -import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' -import { useModalContext } from '@/context/modal-context' -import { FlowType } from '@/types/common' -import { canFindTool } from '@/utils' -import { debounce } from 'lodash-es' -import BeforeRunForm from '../before-run-form' -import PanelWrap from '../before-run-form/panel-wrap' +import { DescriptionInput, TitleInput } from '../title-description-input' import LastRun from './last-run' import useLastRun from './last-run/use-last-run' +import Tab, { TabType } from './tab' import { TriggerSubscription } from './trigger-subscription' -import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' -import { useAllBuiltInTools } from '@/service/use-tools' const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => { const nodeType = params.payload.type @@ -103,6 +98,7 @@ const BasePanel: FC = ({ children, }) => { const { t } = useTranslation() + const language = useLanguage() const { showMessageLogModal } = useAppStore(useShallow(state => ({ showMessageLogModal: state.showMessageLogModal, }))) @@ -224,6 +220,7 @@ const BasePanel: FC = ({ useEffect(() => { hasClickRunning.current = false }, [id]) + const { nodesMap, } = useNodesMetaData() @@ -278,12 +275,7 @@ const BasePanel: FC = ({ }, [pendingSingleRun, id, handleSingleRun, handleStop, setPendingSingleRun]) const logParams = useLogs() - const passedLogParams = (() => { - if ([BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type)) - return logParams - - return {} - })() + const passedLogParams = useMemo(() => [BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type) ? logParams : {}, [data.type, logParams]) const storeBuildInTools = useStore(s => s.buildInTools) const { data: buildInTools } = useAllBuiltInTools() @@ -295,16 +287,32 @@ const BasePanel: FC = ({ return data.type === BlockEnum.Tool && currToolCollection?.allow_delete }, [data.type, currToolCollection?.allow_delete]) - const { data: triggerProviders = [] } = useAllTriggerPlugins() - const currentTriggerProvider = useMemo(() => { - if (!data.provider_id || !data.provider_name) + // only fetch trigger plugins when the node is a trigger plugin + const { data: triggerPlugins = [] } = useAllTriggerPlugins(data.type === BlockEnum.TriggerPlugin) + const currentTriggerPlugin = useMemo(() => { + if (data.type !== BlockEnum.TriggerPlugin || !data.plugin_id || !triggerPlugins?.length) return undefined - return triggerProviders.find(p => p.name === data.provider_id) // todo: confirm - }, [data.type, data.provider_id, data.provider_name, triggerProviders]) + return triggerPlugins?.find(p => p.plugin_id === data.plugin_id) + }, [data.type, data.plugin_id, triggerPlugins]) + const { setDetail } = usePluginStore() - const showTriggerConfig = useMemo(() => { - return data.type === BlockEnum.TriggerPlugin && currentTriggerProvider - }, [data.type, currentTriggerProvider]) + useEffect(() => { + if (currentTriggerPlugin) { + setDetail({ + name: currentTriggerPlugin.label[language], + plugin_id: currentTriggerPlugin.plugin_id || '', + provider: currentTriggerPlugin.name, + declaration: { + tool: undefined, + // @ts-expect-error just remain the necessary fields + trigger: { + subscription_schema: currentTriggerPlugin.subscription_schema || [], + subscription_constructor: currentTriggerPlugin.subscription_constructor, + }, + }, + }) + } + }, [currentTriggerPlugin, setDetail]) const dataSourceList = useStore(s => s.dataSourceList) @@ -352,14 +360,14 @@ const BasePanel: FC = ({ pluginDetail = currentDataSource break case BlockEnum.TriggerPlugin: - pluginDetail = currentTriggerProvider + pluginDetail = currentTriggerPlugin break default: break } return !pluginDetail ? null : - }, [data.type, currToolCollection, currentDataSource, currentTriggerProvider]) + }, [data.type, currToolCollection, currentDataSource, currentTriggerPlugin]) if (logParams.showSpecialResultPanel) { return ( @@ -558,9 +566,9 @@ const BasePanel: FC = ({ ) } { - showTriggerConfig && ( + currentTriggerPlugin && ( = ({ ) } { - !needsToolAuth && !currentDataSource && !showTriggerConfig && ( + !needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
void) => void children: React.ReactNode } -export const TriggerSubscription: FC = ({ data, onSubscriptionChange, children }) => { - // @ts-expect-error TODO: fix this - const { currentProvider } = useConfig(data.id as string, data) - const { setDetail } = usePluginStore() - const language = useLanguage() +export const TriggerSubscription: FC = ({ subscriptionIdSelected, onSubscriptionChange, children }) => { const { subscriptions } = useSubscriptionList() const subscriptionCount = subscriptions?.length || 0 - useEffect(() => { - if (currentProvider) { - setDetail({ - name: currentProvider.label[language], - plugin_id: currentProvider.plugin_id || '', - provider: currentProvider.name, - declaration: { - tool: undefined, - // @ts-expect-error just remain the necessary fields - trigger: { - subscription_schema: currentProvider.subscription_schema || [], - subscription_constructor: currentProvider.subscription_constructor, - }, - }, - }) - } - }, [currentProvider, setDetail]) - return
0 && 'flex items-center justify-between pr-3')}> {!subscriptionCount && } {children} {subscriptionCount > 0 && }
diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx index 17c71b6b95..93bf788c34 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx @@ -14,7 +14,7 @@ type Props = { onChange: (value: PluginTriggerVarInputs) => void onOpen?: (index: number) => void inPanel?: boolean - currentTrigger?: Event + currentEvent?: Event currentProvider?: TriggerWithProvider extraParams?: Record disableVariableInsertion?: boolean @@ -27,7 +27,7 @@ const TriggerForm: FC = ({ value, onChange, inPanel, - currentTrigger, + currentEvent, currentProvider, extraParams, disableVariableInsertion = false, @@ -44,7 +44,7 @@ const TriggerForm: FC = ({ value={value} onChange={onChange} inPanel={inPanel} - currentTrigger={currentTrigger} + currentEvent={currentEvent} currentProvider={currentProvider} extraParams={extraParams} disableVariableInsertion={disableVariableInsertion} diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx index 22331aa578..678c12f02a 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx @@ -22,7 +22,7 @@ type Props = { value: PluginTriggerVarInputs onChange: (value: PluginTriggerVarInputs) => void inPanel?: boolean - currentTrigger?: Event + currentEvent?: Event currentProvider?: TriggerWithProvider extraParams?: Record disableVariableInsertion?: boolean @@ -35,7 +35,7 @@ const TriggerFormItem: FC = ({ value, onChange, inPanel, - currentTrigger, + currentEvent, currentProvider, extraParams, disableVariableInsertion = false, @@ -91,7 +91,7 @@ const TriggerFormItem: FC = ({ value={value} onChange={onChange} inPanel={inPanel} - currentTool={currentTrigger} + currentTool={currentEvent} currentProvider={currentProvider} providerType='trigger' extraParams={extraParams} diff --git a/web/app/components/workflow/nodes/trigger-plugin/panel.tsx b/web/app/components/workflow/nodes/trigger-plugin/panel.tsx index f7dc8374e7..9b4d8058b1 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/panel.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/panel.tsx @@ -22,7 +22,8 @@ const Panel: FC> = ({ outputSchema, hasObjectOutput, currentProvider, - currentTrigger, + currentEvent, + subscriptionSelected, } = useConfig(id, data) const disableVariableInsertion = data.type === BlockEnum.TriggerPlugin @@ -36,7 +37,7 @@ const Panel: FC> = ({ return (
{/* Dynamic Parameters Form - Only show when authenticated */} - {triggerParameterSchema.length > 0 && ( + {triggerParameterSchema.length > 0 && subscriptionSelected && ( <>
> = ({ value={triggerParameterValue} onChange={setTriggerParameterValue} currentProvider={currentProvider} - currentTrigger={currentTrigger} + currentEvent={currentEvent} disableVariableInsertion={disableVariableInsertion} />
diff --git a/web/app/components/workflow/nodes/trigger-plugin/use-config.ts b/web/app/components/workflow/nodes/trigger-plugin/use-config.ts index 7d75aa5e00..6ea711cd31 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/use-config.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/use-config.ts @@ -86,6 +86,7 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => { event_name: event_name, config = {}, event_parameters: rawEventParameters = {}, + subscription_id, } = inputs const event_parameters = useMemo( @@ -97,16 +98,6 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => { [config], ) - // Construct provider for authentication check - const authProvider = useMemo(() => { - return provider_name || '' - }, [provider_id, provider_name]) - - const { data: subscriptions = [] } = useTriggerSubscriptions( - authProvider, - !!authProvider, - ) - const currentProvider = useMemo(() => { return triggerPlugins.find( provider => @@ -116,6 +107,12 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => { ) }, [triggerPlugins, provider_name, provider_id]) + const { data: subscriptions = [] } = useTriggerSubscriptions(provider_id || '') + + const subscriptionSelected = useMemo(() => { + return subscriptions?.find(s => s.id === subscription_id) + }, [subscriptions, subscription_id]) + const currentEvent = useMemo(() => { return currentProvider?.events.find( event => event.name === event_name, @@ -221,7 +218,7 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => { readOnly, inputs, currentProvider, - currentTrigger: currentEvent, + currentEvent, triggerParameterSchema, triggerParameterValue, setTriggerParameterValue, @@ -229,6 +226,7 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => { outputSchema, hasObjectOutput, subscriptions, + subscriptionSelected, } } From 6e0765fbaf6313a7f87ccf12c16c55c30bf31767 Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Thu, 30 Oct 2025 14:15:30 +0800 Subject: [PATCH 03/17] feat: add install check for tools, triggers and datasources --- .../workflow/block-selector/data-sources.tsx | 1 + .../block-selector/tool/action-item.tsx | 1 + .../workflow/block-selector/tool/tool.tsx | 2 + .../workflow/block-selector/types.ts | 2 + .../hooks/use-node-plugin-installation.ts | 208 ++++++++++++++++++ .../workflow/nodes/data-source/node.tsx | 32 ++- .../workflow/nodes/data-source/types.ts | 1 + .../components/workflow/nodes/tool/node.tsx | 78 ++++--- .../components/workflow/nodes/tool/types.ts | 1 + .../workflow/nodes/trigger-plugin/node.tsx | 22 +- .../workflow/nodes/trigger-plugin/types.ts | 1 + web/app/components/workflow/types.ts | 1 + 12 files changed, 318 insertions(+), 32 deletions(-) create mode 100644 web/app/components/workflow/hooks/use-node-plugin-installation.ts diff --git a/web/app/components/workflow/block-selector/data-sources.tsx b/web/app/components/workflow/block-selector/data-sources.tsx index 3961f63dbe..b98a52dcff 100644 --- a/web/app/components/workflow/block-selector/data-sources.tsx +++ b/web/app/components/workflow/block-selector/data-sources.tsx @@ -63,6 +63,7 @@ const DataSources = ({ datasource_name: toolDefaultValue?.tool_name, datasource_label: toolDefaultValue?.tool_label, title: toolDefaultValue?.title, + plugin_unique_identifier: toolDefaultValue?.plugin_unique_identifier, } // Update defaultValue with fileExtensions if this is the local file data source if (toolDefaultValue?.provider_id === 'langgenius/file' && toolDefaultValue?.provider_name === 'file') { diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index e2c28602f8..01c319327a 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -72,6 +72,7 @@ const ToolItem: FC = ({ provider_type: provider.type, provider_name: provider.name, plugin_id: provider.plugin_id, + plugin_unique_identifier: provider.plugin_unique_identifier, provider_icon: normalizeProviderIcon(provider.icon), tool_name: payload.name, tool_label: payload.label[language], diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index 5ac043e933..38be8d19d6 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -94,6 +94,7 @@ const Tool: FC = ({ provider_type: payload.type, provider_name: payload.name, plugin_id: payload.plugin_id, + plugin_unique_identifier: payload.plugin_unique_identifier, provider_icon: normalizeProviderIcon(payload.icon), tool_name: tool.name, tool_label: tool.label[language], @@ -175,6 +176,7 @@ const Tool: FC = ({ provider_type: payload.type, provider_name: payload.name, plugin_id: payload.plugin_id, + plugin_unique_identifier: payload.plugin_unique_identifier, provider_icon: normalizeProviderIcon(payload.icon), tool_name: tool.name, tool_label: tool.label[language], diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 512621a552..e995974b87 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -59,6 +59,7 @@ export type ToolDefaultValue = PluginCommonDefaultValue & { meta?: PluginMeta plugin_id?: string provider_icon?: Collection['icon'] + plugin_unique_identifier?: string } export type DataSourceDefaultValue = Omit & { @@ -69,6 +70,7 @@ export type DataSourceDefaultValue = Omit void + shouldDim: boolean +} + +const useToolInstallation = (data: ToolNodeType): InstallationState => { + const builtInQuery = useAllBuiltInTools() + const customQuery = useAllCustomTools() + const workflowQuery = useAllWorkflowTools() + const mcpQuery = useAllMCPTools() + const invalidateTools = useInvalidToolsByType(data.provider_type) + + const collectionInfo = useMemo(() => { + switch (data.provider_type) { + case CollectionType.builtIn: + return { + list: builtInQuery.data, + isLoading: builtInQuery.isLoading, + } + case CollectionType.custom: + return { + list: customQuery.data, + isLoading: customQuery.isLoading, + } + case CollectionType.workflow: + return { + list: workflowQuery.data, + isLoading: workflowQuery.isLoading, + } + case CollectionType.mcp: + return { + list: mcpQuery.data, + isLoading: mcpQuery.isLoading, + } + default: + return undefined + } + }, [ + builtInQuery.data, + builtInQuery.isLoading, + customQuery.data, + customQuery.isLoading, + data.provider_type, + mcpQuery.data, + mcpQuery.isLoading, + workflowQuery.data, + workflowQuery.isLoading, + ]) + + const collection = collectionInfo?.list + const isLoading = collectionInfo?.isLoading ?? false + const isResolved = !!collectionInfo && !isLoading + + const matchedCollection = useMemo(() => { + if (!collection || !collection.length) + return undefined + + return collection.find((toolWithProvider) => { + if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id) + return true + if (canFindTool(toolWithProvider.id, data.provider_id)) + return true + if (toolWithProvider.name === data.provider_name) + return true + return false + }) + }, [collection, data.plugin_id, data.provider_id, data.provider_name]) + + const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id + const canInstall = Boolean(data.plugin_unique_identifier) + + const onInstallSuccess = useCallback(() => { + if (invalidateTools) + invalidateTools() + }, [invalidateTools]) + + return { + isChecking: !!collectionInfo && !isResolved, + isMissing: isResolved && !matchedCollection, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } +} + +const useTriggerInstallation = (data: PluginTriggerNodeType): InstallationState => { + const triggerPluginsQuery = useAllTriggerPlugins() + const invalidateTriggers = useInvalidateAllTriggerPlugins() + + const triggerProviders = triggerPluginsQuery.data + const isLoading = triggerPluginsQuery.isLoading + + const matchedProvider = useMemo(() => { + if (!triggerProviders || !triggerProviders.length) + return undefined + + return triggerProviders.find(provider => + provider.name === data.provider_name + || provider.id === data.provider_id + || (data.plugin_id && provider.plugin_id === data.plugin_id), + ) + }, [ + data.plugin_id, + data.provider_id, + data.provider_name, + triggerProviders, + ]) + + const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id + const canInstall = Boolean(data.plugin_unique_identifier) + + const onInstallSuccess = useCallback(() => { + invalidateTriggers() + }, [invalidateTriggers]) + + return { + isChecking: isLoading, + isMissing: !isLoading && !!triggerProviders && !matchedProvider, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } +} + +const useDataSourceInstallation = (data: DataSourceNodeType): InstallationState => { + const dataSourceList = useStore(s => s.dataSourceList) + const invalidateDataSourceList = useInvalidDataSourceList() + + const matchedPlugin = useMemo(() => { + if (!dataSourceList || !dataSourceList.length) + return undefined + + return dataSourceList.find((item) => { + if (data.plugin_unique_identifier && item.plugin_unique_identifier === data.plugin_unique_identifier) + return true + if (data.plugin_id && item.plugin_id === data.plugin_id) + return true + if (data.provider_name && item.provider === data.provider_name) + return true + return false + }) + }, [data.plugin_id, data.plugin_unique_identifier, data.provider_name, dataSourceList]) + + const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id + const canInstall = Boolean(data.plugin_unique_identifier) + + const onInstallSuccess = useCallback(() => { + invalidateDataSourceList() + }, [invalidateDataSourceList]) + + const hasLoadedList = dataSourceList !== undefined + + return { + isChecking: !hasLoadedList, + isMissing: hasLoadedList && !matchedPlugin, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } +} + +export const useNodePluginInstallation = (data: CommonNodeType): InstallationState => { + const toolInstallation = useToolInstallation(data as ToolNodeType) + const triggerInstallation = useTriggerInstallation(data as PluginTriggerNodeType) + const dataSourceInstallation = useDataSourceInstallation(data as DataSourceNodeType) + + switch (data.type as BlockEnum) { + case BlockEnum.Tool: + return toolInstallation + case BlockEnum.TriggerPlugin: + return triggerInstallation + case BlockEnum.DataSource: + return dataSourceInstallation + default: + return { + isChecking: false, + isMissing: false, + uniqueIdentifier: undefined, + canInstall: false, + onInstallSuccess: () => undefined, + } + } +} diff --git a/web/app/components/workflow/nodes/data-source/node.tsx b/web/app/components/workflow/nodes/data-source/node.tsx index f97098e52f..6e6c565dc2 100644 --- a/web/app/components/workflow/nodes/data-source/node.tsx +++ b/web/app/components/workflow/nodes/data-source/node.tsx @@ -1,10 +1,36 @@ import type { FC } from 'react' import { memo } from 'react' -import type { DataSourceNodeType } from './types' import type { NodeProps } from '@/app/components/workflow/types' -const Node: FC> = () => { +import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' +import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation' +import type { DataSourceNodeType } from './types' + +const Node: FC> = ({ + data, +}) => { + const { + isChecking, + isMissing, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } = useNodePluginInstallation(data) + + const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier + + if (!showInstallButton) + return null + return ( -
+
+
+ +
) } diff --git a/web/app/components/workflow/nodes/data-source/types.ts b/web/app/components/workflow/nodes/data-source/types.ts index da887244b8..cd22b305d1 100644 --- a/web/app/components/workflow/nodes/data-source/types.ts +++ b/web/app/components/workflow/nodes/data-source/types.ts @@ -30,6 +30,7 @@ export type DataSourceNodeType = CommonNodeType & { datasource_label: string datasource_parameters: ToolVarInputs datasource_configurations: Record + plugin_unique_identifier?: string } export type CustomRunFormProps = { diff --git a/web/app/components/workflow/nodes/tool/node.tsx b/web/app/components/workflow/nodes/tool/node.tsx index 8cc3ec580d..466cbb577f 100644 --- a/web/app/components/workflow/nodes/tool/node.tsx +++ b/web/app/components/workflow/nodes/tool/node.tsx @@ -1,46 +1,68 @@ import type { FC } from 'react' import React from 'react' -import type { ToolNodeType } from './types' import type { NodeProps } from '@/app/components/workflow/types' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' +import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation' +import type { ToolNodeType } from './types' const Node: FC> = ({ data, }) => { const { tool_configurations, paramSchemas } = data const toolConfigs = Object.keys(tool_configurations || {}) + const { + isChecking, + isMissing, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } = useNodePluginInstallation(data) + const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier - if (!toolConfigs.length) + const hasConfigs = toolConfigs.length > 0 + + if (!showInstallButton && !hasConfigs) return null return ( -
-
- {toolConfigs.map((key, index) => ( -
-
- {key} +
+ {showInstallButton && ( +
+ +
+ )} + {hasConfigs && ( +
+ {toolConfigs.map((key, index) => ( +
+
+ {key} +
+ {typeof tool_configurations[key].value === 'string' && ( +
+ {paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value} +
+ )} + {typeof tool_configurations[key].value === 'number' && ( +
+ {Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} +
+ )} + {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && ( +
+ {tool_configurations[key].model} +
+ )}
- {typeof tool_configurations[key].value === 'string' && ( -
- {paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value} -
- )} - {typeof tool_configurations[key].value === 'number' && ( -
- {Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} -
- )} - {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && ( -
- {tool_configurations[key].model} -
- )} -
- - ))} - -
+ ))} +
+ )}
) } diff --git a/web/app/components/workflow/nodes/tool/types.ts b/web/app/components/workflow/nodes/tool/types.ts index 12a4283cf6..6e6ef858dc 100644 --- a/web/app/components/workflow/nodes/tool/types.ts +++ b/web/app/components/workflow/nodes/tool/types.ts @@ -22,4 +22,5 @@ export type ToolNodeType = CommonNodeType & { params?: Record plugin_id?: string provider_icon?: Collection['icon'] + plugin_unique_identifier?: string } diff --git a/web/app/components/workflow/nodes/trigger-plugin/node.tsx b/web/app/components/workflow/nodes/trigger-plugin/node.tsx index 9be517e97d..bfd807f30e 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/node.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/node.tsx @@ -3,6 +3,8 @@ import type { NodeProps } from '@/app/components/workflow/types' import type { FC } from 'react' import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' +import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation' import type { PluginTriggerNodeType } from './types' import useConfig from './use-config' @@ -42,6 +44,14 @@ const Node: FC> = ({ const { subscriptions } = useConfig(id, data) const { config = {}, subscription_id } = data const configKeys = Object.keys(config) + const { + isChecking, + isMissing, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } = useNodePluginInstallation(data) + const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier const { t } = useTranslation() @@ -50,7 +60,17 @@ const Node: FC> = ({ }, [subscription_id, subscriptions]) return ( -
+
+ {showInstallButton && ( +
+ +
+ )}
{!isValidSubscription && } {isValidSubscription && configKeys.map((key, index) => ( diff --git a/web/app/components/workflow/nodes/trigger-plugin/types.ts b/web/app/components/workflow/nodes/trigger-plugin/types.ts index 43268e9096..6dba97d795 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/types.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/types.ts @@ -16,6 +16,7 @@ export type PluginTriggerNodeType = CommonNodeType & { event_node_version?: string plugin_id?: string config?: Record + plugin_unique_identifier?: string } // Use base types directly diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index a4e7002960..d126daa350 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -451,6 +451,7 @@ export type MoreInfo = { export type ToolWithProvider = Collection & { tools: Tool[] meta: PluginMeta + plugin_unique_identifier?: string } export type RAGRecommendedPlugins = { From ff0f645e543cf60efccb7f4ecfd79654f8f61734 Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Thu, 30 Oct 2025 14:42:52 +0800 Subject: [PATCH 04/17] Fix plugin install detection for tool nodes --- .../components/install-plugin-button.tsx | 24 +++++++++++++++---- .../workflow/nodes/data-source/node.tsx | 4 ++++ .../components/workflow/nodes/tool/node.tsx | 5 ++++ .../workflow/nodes/trigger-plugin/node.tsx | 5 ++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx b/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx index 23119f0213..c387cb8630 100644 --- a/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx +++ b/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx @@ -7,15 +7,25 @@ import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/u type InstallPluginButtonProps = Omit, 'children' | 'loading'> & { uniqueIdentifier: string + extraIdentifiers?: string[] onSuccess?: () => void } export const InstallPluginButton = (props: InstallPluginButtonProps) => { - const { className, uniqueIdentifier, onSuccess, ...rest } = props + const { + className, + uniqueIdentifier, + extraIdentifiers = [], + onSuccess, + ...rest + } = props const { t } = useTranslation() + const identifiers = Array.from(new Set( + [uniqueIdentifier, ...extraIdentifiers].filter((item): item is string => Boolean(item)), + )) const manifest = useCheckInstalled({ - pluginIds: [uniqueIdentifier], - enabled: !!uniqueIdentifier, + pluginIds: identifiers, + enabled: identifiers.length > 0, }) const install = useInstallPackageFromMarketPlace() const isLoading = manifest.isLoading || install.isPending @@ -31,7 +41,13 @@ export const InstallPluginButton = (props: InstallPluginButtonProps) => { }) } if (!manifest.data) return null - if (manifest.data.plugins.some(plugin => plugin.id === uniqueIdentifier)) return null + const identifierSet = new Set(identifiers) + const isInstalled = manifest.data.plugins.some(plugin => ( + identifierSet.has(plugin.id) + || (plugin.plugin_unique_identifier && identifierSet.has(plugin.plugin_unique_identifier)) + || (plugin.plugin_id && identifierSet.has(plugin.plugin_id)) + )) + if (isInstalled) return null return