feat: propagate trigger metadata for plugin icons across UI

This commit is contained in:
lyzno1 2025-10-25 12:15:21 +08:00
parent e3484c8dc3
commit 3bd62f3fdf
No known key found for this signature in database
9 changed files with 258 additions and 29 deletions

View File

@ -5,16 +5,17 @@ from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config
from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required from controllers.console.wraps import account_initialization_required, setup_required
from core.trigger.trigger_manager import TriggerManager
from extensions.ext_database import db from extensions.ext_database import db
from fields.workflow_trigger_fields import trigger_fields, triggers_list_fields, webhook_trigger_fields from fields.workflow_trigger_fields import trigger_fields, triggers_list_fields, webhook_trigger_fields
from libs.login import current_user, login_required 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.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__) logger = logging.getLogger(__name__)
@ -80,13 +81,64 @@ class AppTriggersApi(Resource):
.all() .all()
) )
# Add computed icon field for each trigger plugin_node_ids = [
url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/" 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: for trigger in triggers:
if trigger.trigger_type == "trigger-plugin": if trigger.trigger_type == AppTriggerType.TRIGGER_PLUGIN.value:
trigger.icon = url_prefix + trigger.provider_name + "/icon" # type: ignore 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: else:
trigger.icon = "" # type: ignore trigger.icon = "" # type: ignore[attr-defined]
return {"data": triggers} return {"data": triggers}
@ -130,12 +182,31 @@ class AppTriggerEnableApi(Resource):
session.commit() session.commit()
session.refresh(trigger) session.refresh(trigger)
# Add computed icon field if trigger.trigger_type == AppTriggerType.TRIGGER_PLUGIN.value:
url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/" plugin_icon = ""
if trigger.trigger_type == "trigger-plugin": with Session(db.engine) as session:
trigger.icon = url_prefix + trigger.provider_name + "/icon" # type: ignore 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: else:
trigger.icon = "" # type: ignore trigger.icon = "" # type: ignore[attr-defined]
return trigger return trigger

View File

@ -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_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_by_end_user": fields.Nested(simple_end_user_fields, attribute="created_by_end_user", allow_null=True),
"created_at": TimestampField, "created_at": TimestampField,
"trigger_info": fields.Raw(attribute="trigger_info"),
} }

View File

@ -6,6 +6,11 @@ trigger_fields = {
"title": fields.String, "title": fields.String,
"node_id": fields.String, "node_id": fields.String,
"provider_name": 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, "icon": fields.String,
"status": fields.String, "status": fields.String,
"created_at": fields.DateTime(dt_format="iso8601"), "created_at": fields.DateTime(dt_format="iso8601"),

View File

@ -1,12 +1,16 @@
import json
import uuid import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import and_, func, or_, select from sqlalchemy import and_, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from core.trigger.trigger_manager import TriggerManager
from core.workflow.enums import WorkflowExecutionStatus from core.workflow.enums import WorkflowExecutionStatus
from models import Account, App, EndUser, WorkflowAppLog, WorkflowRun 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: class WorkflowAppService:
@ -111,6 +115,10 @@ class WorkflowAppService:
# Execute query and get items # Execute query and get items
items = list(session.scalars(offset_stmt).all()) 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 { return {
"page": page, "page": page,
"limit": limit, "limit": limit,
@ -129,3 +137,101 @@ class WorkflowAppService:
return uuid.UUID(value) return uuid.UUID(value)
except ValueError: except ValueError:
return None 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

View File

@ -26,7 +26,13 @@ export type ITriggerCardProps = {
} }
const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => { 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 // Status dot styling based on trigger status
const getStatusDot = () => { const getStatusDot = () => {
@ -58,16 +64,18 @@ const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => {
blockType = BlockEnum.TriggerWebhook blockType = BlockEnum.TriggerWebhook
} }
let triggerIcon: string | undefined let triggerIcon: string | undefined = triggerIconFromApi
if (trigger_type === 'trigger-plugin' && provider_name) { if (!triggerIcon && trigger_type === 'trigger-plugin') {
const targetTriggers = triggerPlugins || [] const targetTriggers = triggerPlugins || []
const foundTrigger = targetTriggers.find(triggerWithProvider => const identifiers = [provider_id, provider_name].filter(Boolean) as string[]
canFindTool(triggerWithProvider.id, provider_name) const foundTrigger = targetTriggers.find((triggerWithProvider) => {
|| triggerWithProvider.id.includes(provider_name) return identifiers.some(identifier =>
|| triggerWithProvider.name === provider_name, canFindTool(triggerWithProvider.id, identifier)
) || triggerWithProvider.id.includes(identifier)
|| triggerWithProvider.name === identifier,
)
})
triggerIcon = foundTrigger?.icon triggerIcon = foundTrigger?.icon
console.log('triggerIcon', triggerIcon)
} }
return ( return (

View File

@ -162,7 +162,10 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
</td> </td>
{isWorkflow && ( {isWorkflow && (
<td className='p-3 pr-2'> <td className='p-3 pr-2'>
<TriggerByDisplay triggeredFrom={log.workflow_run.triggered_from || 'app-run'} /> <TriggerByDisplay
triggeredFrom={log.workflow_run.triggered_from || 'app-run'}
triggerInfo={log.trigger_info}
/>
</td> </td>
)} )}
</tr> </tr>

View File

@ -11,11 +11,26 @@ import {
} from '@/app/components/base/icons/src/vender/workflow' } from '@/app/components/base/icons/src/vender/workflow'
import BlockIcon from '@/app/components/workflow/block-icon' import BlockIcon from '@/app/components/workflow/block-icon'
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
import type { TriggerInfo } from '@/models/log'
type TriggerByDisplayProps = { type TriggerByDisplayProps = {
triggeredFrom: string triggeredFrom: string
className?: string className?: string
showText?: boolean 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) => { const getTriggerDisplayName = (triggeredFrom: string, t: any) => {
@ -32,7 +47,7 @@ const getTriggerDisplayName = (triggeredFrom: string, t: any) => {
return nameMap[triggeredFrom] || triggeredFrom return nameMap[triggeredFrom] || triggeredFrom
} }
const getTriggerIcon = (triggeredFrom: string) => { const getTriggerIcon = (triggeredFrom: string, triggerInfo?: TriggerInfo) => {
switch (triggeredFrom) { switch (triggeredFrom) {
case 'webhook': case 'webhook':
return ( return (
@ -47,12 +62,11 @@ const getTriggerIcon = (triggeredFrom: string) => {
</div> </div>
) )
case 'plugin': 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 ( return (
<BlockIcon <BlockIcon
type={BlockEnum.TriggerPlugin} type={BlockEnum.TriggerPlugin}
size="md" size="md"
toolIcon={triggerInfo?.icon}
/> />
) )
case 'debugging': case 'debugging':
@ -83,11 +97,13 @@ const TriggerByDisplay: FC<TriggerByDisplayProps> = ({
triggeredFrom, triggeredFrom,
className = '', className = '',
showText = true, showText = true,
triggerInfo,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const displayName = getTriggerDisplayName(triggeredFrom, t) const resolvedType = resolveTriggerType(triggerInfo?.type || triggeredFrom)
const icon = getTriggerIcon(triggeredFrom) const displayName = triggerInfo?.provider_name || getTriggerDisplayName(resolvedType, t)
const icon = getTriggerIcon(resolvedType, triggerInfo)
return ( return (
<div className={`flex items-center gap-1.5 ${className}`}> <div className={`flex items-center gap-1.5 ${className}`}>

View File

@ -242,6 +242,19 @@ export type WorkflowRunDetail = {
total_steps: number total_steps: number
finished_at: 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 = { export type AccountInfo = {
id: string id: string
name: string name: string
@ -262,6 +275,7 @@ export type WorkflowAppLogDetail = {
created_by_end_user?: EndUserInfo created_by_end_user?: EndUserInfo
created_at: number created_at: number
read_at?: number read_at?: number
trigger_info?: TriggerInfo
} }
export type WorkflowLogsResponse = { export type WorkflowLogsResponse = {
data: Array<WorkflowAppLogDetail> data: Array<WorkflowAppLogDetail>

View File

@ -335,6 +335,11 @@ export type AppTrigger = {
title: string title: string
node_id: string node_id: string
provider_name: string provider_name: string
provider_id?: string
subscription_id?: string
plugin_id?: string
plugin_unique_identifier?: string
event_name?: string
icon: string icon: string
status: 'enabled' | 'disabled' | 'unauthorized' status: 'enabled' | 'disabled' | 'unauthorized'
created_at: string created_at: string