diff --git a/api/controllers/console/app/workflow_trigger.py b/api/controllers/console/app/workflow_trigger.py index fd64261525..cd9f60cd6e 100644 --- a/api/controllers/console/app/workflow_trigger.py +++ b/api/controllers/console/app/workflow_trigger.py @@ -5,16 +5,17 @@ from sqlalchemy import select from sqlalchemy.orm import Session from werkzeug.exceptions import Forbidden, NotFound -from configs import dify_config from controllers.console import api from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required +from core.trigger.trigger_manager import TriggerManager from extensions.ext_database import db from fields.workflow_trigger_fields import trigger_fields, triggers_list_fields, webhook_trigger_fields from libs.login import current_user, login_required -from models.enums import AppTriggerStatus +from models.enums import AppTriggerStatus, AppTriggerType from models.model import Account, App, AppMode -from models.trigger import AppTrigger, WorkflowWebhookTrigger +from models.provider_ids import TriggerProviderID +from models.trigger import AppTrigger, WorkflowPluginTrigger, WorkflowWebhookTrigger logger = logging.getLogger(__name__) @@ -80,13 +81,64 @@ class AppTriggersApi(Resource): .all() ) - # Add computed icon field for each trigger - url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/" + plugin_node_ids = [ + trigger.node_id for trigger in triggers if trigger.trigger_type == AppTriggerType.TRIGGER_PLUGIN.value + ] + plugin_trigger_map: dict[str, WorkflowPluginTrigger] = {} + if plugin_node_ids: + plugin_triggers = ( + session.execute( + select(WorkflowPluginTrigger).where( + WorkflowPluginTrigger.app_id == app_model.id, + WorkflowPluginTrigger.node_id.in_(plugin_node_ids), + ) + ) + .scalars() + .all() + ) + plugin_trigger_map = {plugin_trigger.node_id: plugin_trigger for plugin_trigger in plugin_triggers} + + tenant_id = current_user.current_tenant_id if isinstance(current_user, Account) else None + provider_cache: dict[str, dict[str, str]] = {} + + def resolve_provider_metadata(provider_id: str) -> dict[str, str]: + if provider_id in provider_cache: + return provider_cache[provider_id] + metadata: dict[str, str] = {} + if not tenant_id: + provider_cache[provider_id] = metadata + return metadata + try: + controller = TriggerManager.get_trigger_provider(tenant_id, TriggerProviderID(provider_id)) + api_entity = controller.to_api_entity() + metadata = { + "plugin_id": controller.plugin_id, + "plugin_unique_identifier": controller.plugin_unique_identifier, + "icon": api_entity.icon or "", + "provider_name": api_entity.name, + } + except Exception: + metadata = {} + provider_cache[provider_id] = metadata + return metadata + for trigger in triggers: - if trigger.trigger_type == "trigger-plugin": - trigger.icon = url_prefix + trigger.provider_name + "/icon" # type: ignore + if trigger.trigger_type == AppTriggerType.TRIGGER_PLUGIN.value: + plugin_trigger = plugin_trigger_map.get(trigger.node_id) + if not plugin_trigger: + trigger.icon = "" # type: ignore[attr-defined] + continue + trigger.provider_id = plugin_trigger.provider_id # type: ignore[attr-defined] + trigger.subscription_id = plugin_trigger.subscription_id # type: ignore[attr-defined] + trigger.event_name = plugin_trigger.event_name # type: ignore[attr-defined] + metadata = resolve_provider_metadata(plugin_trigger.provider_id) + trigger.plugin_id = metadata.get("plugin_id") # type: ignore[attr-defined] + trigger.plugin_unique_identifier = metadata.get("plugin_unique_identifier") # type: ignore[attr-defined] + trigger.icon = metadata.get("icon", "") # type: ignore[attr-defined] + if not trigger.provider_name: + trigger.provider_name = metadata.get("provider_name", "") else: - trigger.icon = "" # type: ignore + trigger.icon = "" # type: ignore[attr-defined] return {"data": triggers} @@ -130,12 +182,31 @@ class AppTriggerEnableApi(Resource): session.commit() session.refresh(trigger) - # Add computed icon field - url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/" - if trigger.trigger_type == "trigger-plugin": - trigger.icon = url_prefix + trigger.provider_name + "/icon" # type: ignore + if trigger.trigger_type == AppTriggerType.TRIGGER_PLUGIN.value: + plugin_icon = "" + with Session(db.engine) as session: + plugin_trigger = session.execute( + select(WorkflowPluginTrigger).where( + WorkflowPluginTrigger.app_id == app_model.id, + WorkflowPluginTrigger.node_id == trigger.node_id, + ) + ).scalar_one_or_none() + if plugin_trigger and current_user.current_tenant_id: + try: + controller = TriggerManager.get_trigger_provider( + current_user.current_tenant_id, TriggerProviderID(plugin_trigger.provider_id) + ) + trigger.provider_id = plugin_trigger.provider_id # type: ignore[attr-defined] + trigger.subscription_id = plugin_trigger.subscription_id # type: ignore[attr-defined] + trigger.event_name = plugin_trigger.event_name # type: ignore[attr-defined] + trigger.plugin_id = controller.plugin_id # type: ignore[attr-defined] + trigger.plugin_unique_identifier = controller.plugin_unique_identifier # type: ignore[attr-defined] + plugin_icon = controller.to_api_entity().icon or "" + except Exception: + plugin_icon = "" + trigger.icon = plugin_icon # type: ignore[attr-defined] else: - trigger.icon = "" # type: ignore + trigger.icon = "" # type: ignore[attr-defined] return trigger diff --git a/api/fields/workflow_app_log_fields.py b/api/fields/workflow_app_log_fields.py index 243efd817c..45a9c7d0aa 100644 --- a/api/fields/workflow_app_log_fields.py +++ b/api/fields/workflow_app_log_fields.py @@ -13,6 +13,7 @@ workflow_app_log_partial_fields = { "created_by_account": fields.Nested(simple_account_fields, attribute="created_by_account", allow_null=True), "created_by_end_user": fields.Nested(simple_end_user_fields, attribute="created_by_end_user", allow_null=True), "created_at": TimestampField, + "trigger_info": fields.Raw(attribute="trigger_info"), } diff --git a/api/fields/workflow_trigger_fields.py b/api/fields/workflow_trigger_fields.py index ce51d1833a..0d37d3dd85 100644 --- a/api/fields/workflow_trigger_fields.py +++ b/api/fields/workflow_trigger_fields.py @@ -6,6 +6,11 @@ trigger_fields = { "title": fields.String, "node_id": fields.String, "provider_name": fields.String, + "provider_id": fields.String, + "subscription_id": fields.String, + "event_name": fields.String, + "plugin_id": fields.String, + "plugin_unique_identifier": fields.String, "icon": fields.String, "status": fields.String, "created_at": fields.DateTime(dt_format="iso8601"), diff --git a/api/services/workflow_app_service.py b/api/services/workflow_app_service.py index 23dd436675..ca817cee24 100644 --- a/api/services/workflow_app_service.py +++ b/api/services/workflow_app_service.py @@ -1,12 +1,16 @@ +import json import uuid from datetime import datetime from sqlalchemy import and_, func, or_, select from sqlalchemy.orm import Session +from core.trigger.trigger_manager import TriggerManager from core.workflow.enums import WorkflowExecutionStatus from models import Account, App, EndUser, WorkflowAppLog, WorkflowRun -from models.enums import CreatorUserRole +from models.enums import AppTriggerType, CreatorUserRole +from models.provider_ids import TriggerProviderID +from models.trigger import WorkflowPluginTrigger, WorkflowTriggerLog class WorkflowAppService: @@ -111,6 +115,10 @@ class WorkflowAppService: # Execute query and get items items = list(session.scalars(offset_stmt).all()) + trigger_info_map = self._build_trigger_info_map(session, app_model, items) + for log in items: + log.trigger_info = trigger_info_map.get(log.workflow_run_id) + return { "page": page, "limit": limit, @@ -129,3 +137,101 @@ class WorkflowAppService: return uuid.UUID(value) except ValueError: return None + + def _build_trigger_info_map(self, session: Session, app_model: App, logs: list[WorkflowAppLog]) -> dict[str, dict]: + run_ids = [log.workflow_run_id for log in logs if log.workflow_run_id] + if not run_ids: + return {} + + trigger_logs = ( + session.execute(select(WorkflowTriggerLog).where(WorkflowTriggerLog.workflow_run_id.in_(run_ids))) + .scalars() + .all() + ) + if not trigger_logs: + return {} + + trigger_data_map: dict[str, dict] = {} + node_ids: set[str] = set() + for trigger_log in trigger_logs: + if not trigger_log.workflow_run_id: + continue + try: + trigger_data = json.loads(trigger_log.trigger_data) + except json.JSONDecodeError: + trigger_data = {} + node_id = trigger_data.get("root_node_id") + if node_id: + node_ids.add(node_id) + trigger_data_map[trigger_log.workflow_run_id] = { + "log": trigger_log, + "node_id": node_id, + } + + plugin_trigger_map: dict[str, WorkflowPluginTrigger] = {} + if node_ids: + plugin_triggers = ( + session.execute( + select(WorkflowPluginTrigger).where( + WorkflowPluginTrigger.app_id == app_model.id, + WorkflowPluginTrigger.node_id.in_(node_ids), + ) + ) + .scalars() + .all() + ) + plugin_trigger_map = {plugin.node_id: plugin for plugin in plugin_triggers} + + provider_cache: dict[str, dict[str, str]] = {} + + def resolve_provider(provider_id: str) -> dict[str, str]: + if provider_id in provider_cache: + return provider_cache[provider_id] + metadata: dict[str, str] = {} + try: + controller = TriggerManager.get_trigger_provider(app_model.tenant_id, TriggerProviderID(provider_id)) + api_entity = controller.to_api_entity() + metadata = { + "provider_name": api_entity.name, + "icon": api_entity.icon or "", + "plugin_id": controller.plugin_id, + "plugin_unique_identifier": controller.plugin_unique_identifier, + } + except Exception: + metadata = {} + provider_cache[provider_id] = metadata + return metadata + + trigger_info_map: dict[str, dict] = {} + for run_id, context in trigger_data_map.items(): + trigger_log = context["log"] + if isinstance(trigger_log.trigger_type, AppTriggerType): + trigger_type_value = trigger_log.trigger_type.value + else: + trigger_type_value = trigger_log.trigger_type + info = { + "type": trigger_type_value, + "node_id": context["node_id"], + "workflow_trigger_log_id": trigger_log.id, + } + + if ( + trigger_log.trigger_type == AppTriggerType.TRIGGER_PLUGIN # type: ignore[comparison-overlap] + and context["node_id"] + ): + plugin_trigger = plugin_trigger_map.get(context["node_id"]) + if plugin_trigger: + info.update( + { + "provider_id": plugin_trigger.provider_id, + "subscription_id": plugin_trigger.subscription_id, + "event_name": plugin_trigger.event_name, + } + ) + provider_metadata = resolve_provider(plugin_trigger.provider_id) + if provider_metadata: + info.update(provider_metadata) + + trigger_info_map[run_id] = info + + return trigger_info_map diff --git a/web/app/components/app/overview/trigger-card.tsx b/web/app/components/app/overview/trigger-card.tsx index 8946bdc0a5..746f653388 100644 --- a/web/app/components/app/overview/trigger-card.tsx +++ b/web/app/components/app/overview/trigger-card.tsx @@ -26,7 +26,13 @@ export type ITriggerCardProps = { } const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => { - const { trigger_type, status, provider_name } = trigger + const { + trigger_type, + status, + provider_name, + provider_id, + icon: triggerIconFromApi, + } = trigger // Status dot styling based on trigger status const getStatusDot = () => { @@ -58,16 +64,18 @@ const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => { blockType = BlockEnum.TriggerWebhook } - let triggerIcon: string | undefined - if (trigger_type === 'trigger-plugin' && provider_name) { + let triggerIcon: string | undefined = triggerIconFromApi + if (!triggerIcon && trigger_type === 'trigger-plugin') { const targetTriggers = triggerPlugins || [] - const foundTrigger = targetTriggers.find(triggerWithProvider => - canFindTool(triggerWithProvider.id, provider_name) - || triggerWithProvider.id.includes(provider_name) - || triggerWithProvider.name === provider_name, - ) + const identifiers = [provider_id, provider_name].filter(Boolean) as string[] + const foundTrigger = targetTriggers.find((triggerWithProvider) => { + return identifiers.some(identifier => + canFindTool(triggerWithProvider.id, identifier) + || triggerWithProvider.id.includes(identifier) + || triggerWithProvider.name === identifier, + ) + }) triggerIcon = foundTrigger?.icon - console.log('triggerIcon', triggerIcon) } return ( diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx index 6e77e596d9..b92b754a64 100644 --- a/web/app/components/app/workflow-log/list.tsx +++ b/web/app/components/app/workflow-log/list.tsx @@ -162,7 +162,10 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { {isWorkflow && ( - + )} diff --git a/web/app/components/app/workflow-log/trigger-by-display.tsx b/web/app/components/app/workflow-log/trigger-by-display.tsx index e9355c288d..9b98ae7c64 100644 --- a/web/app/components/app/workflow-log/trigger-by-display.tsx +++ b/web/app/components/app/workflow-log/trigger-by-display.tsx @@ -11,11 +11,26 @@ import { } from '@/app/components/base/icons/src/vender/workflow' import BlockIcon from '@/app/components/workflow/block-icon' import { BlockEnum } from '@/app/components/workflow/types' +import type { TriggerInfo } from '@/models/log' type TriggerByDisplayProps = { triggeredFrom: string className?: string showText?: boolean + triggerInfo?: TriggerInfo +} + +const resolveTriggerType = (value: string) => { + switch (value) { + case 'trigger-plugin': + return 'plugin' + case 'trigger-webhook': + return 'webhook' + case 'trigger-schedule': + return 'schedule' + default: + return value + } } const getTriggerDisplayName = (triggeredFrom: string, t: any) => { @@ -32,7 +47,7 @@ const getTriggerDisplayName = (triggeredFrom: string, t: any) => { return nameMap[triggeredFrom] || triggeredFrom } -const getTriggerIcon = (triggeredFrom: string) => { +const getTriggerIcon = (triggeredFrom: string, triggerInfo?: TriggerInfo) => { switch (triggeredFrom) { case 'webhook': return ( @@ -47,12 +62,11 @@ const getTriggerIcon = (triggeredFrom: string) => { ) case 'plugin': - // For plugin triggers in logs, use a generic plugin icon since we don't have specific plugin info - // This matches the standard BlockIcon styling for TriggerPlugin return ( ) case 'debugging': @@ -83,11 +97,13 @@ const TriggerByDisplay: FC = ({ triggeredFrom, className = '', showText = true, + triggerInfo, }) => { const { t } = useTranslation() - const displayName = getTriggerDisplayName(triggeredFrom, t) - const icon = getTriggerIcon(triggeredFrom) + const resolvedType = resolveTriggerType(triggerInfo?.type || triggeredFrom) + const displayName = triggerInfo?.provider_name || getTriggerDisplayName(resolvedType, t) + const icon = getTriggerIcon(resolvedType, triggerInfo) return (
diff --git a/web/models/log.ts b/web/models/log.ts index 3a16254d63..1b5b7c253e 100644 --- a/web/models/log.ts +++ b/web/models/log.ts @@ -242,6 +242,19 @@ export type WorkflowRunDetail = { total_steps: number finished_at: number } + +export type TriggerInfo = { + type: string + node_id?: string + workflow_trigger_log_id?: string + provider_id?: string + provider_name?: string + subscription_id?: string + event_name?: string + plugin_id?: string + plugin_unique_identifier?: string + icon?: string +} export type AccountInfo = { id: string name: string @@ -262,6 +275,7 @@ export type WorkflowAppLogDetail = { created_by_end_user?: EndUserInfo created_at: number read_at?: number + trigger_info?: TriggerInfo } export type WorkflowLogsResponse = { data: Array diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index 4b2b4cd4ea..a6258d4e1f 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -335,6 +335,11 @@ export type AppTrigger = { title: string node_id: string provider_name: string + provider_id?: string + subscription_id?: string + plugin_id?: string + plugin_unique_identifier?: string + event_name?: string icon: string status: 'enabled' | 'disabled' | 'unauthorized' created_at: string