From ae29c245297f885199b4b38314a046e443e1d020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cls=5F=E5=AE=81=E6=B3=A2=E6=9C=AC=E6=9C=BA?= <908705107@qq.com> Date: Mon, 22 Jun 2026 03:02:08 +0800 Subject: [PATCH] feat: improve workflow run observability --- .../ai_platform/services/workflow_service.py | 18 +- .../composables/useEventHandler.ts | 79 +++++- .../workflow-runs/modules/detail-dialog.vue | 256 ++++++++++++++++++ 3 files changed, 349 insertions(+), 4 deletions(-) diff --git a/backend-fastapi/ai_platform/services/workflow_service.py b/backend-fastapi/ai_platform/services/workflow_service.py index 37b97dc..be7b11e 100644 --- a/backend-fastapi/ai_platform/services/workflow_service.py +++ b/backend-fastapi/ai_platform/services/workflow_service.py @@ -211,6 +211,14 @@ def _resolve_handoff_target( return next_nodes[0] if next_nodes else None +def _execution_log_event_type(status: Optional[str]) -> str: + if status == 'waiting': + return 'waiting_input' + if status == 'failed': + return 'error' + return 'node_complete' + + def _make_execution_log_entry( node_map: Dict[str, Any], node_id: str, @@ -219,6 +227,7 @@ def _make_execution_log_entry( ) -> dict: extra.setdefault('timestamp', datetime.now().isoformat()) metadata = dict(extra.pop('metadata', {}) or {}) + event_type = _execution_log_event_type(extra.get('status')) collaboration = _node_collaboration_metadata(node_map, node_id, node_type) branch_id = extra.get('branch') or extra.get('branch_id') branch_label = extra.get('branch_label') @@ -231,7 +240,7 @@ def _make_execution_log_entry( node_map, node_id, node_type, - event_type='node_complete', + event_type=event_type, status=extra.get('status'), branch_id=branch_id, branch_label=branch_label, @@ -2508,6 +2517,13 @@ class AIWorkflowService: start_time = time.time() result = await node_instance.execute_async(context) elapsed = int((time.time() - start_time) * 1000) + target_node_id = _resolve_handoff_target( + current_node_id, + result, + edge_map, + parallel_edge_map, + node_map, + ) # 处理节点事件 if result.events: diff --git a/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts b/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts index ee28cef..cbe74e2 100644 --- a/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts +++ b/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts @@ -226,6 +226,49 @@ export function useEventHandler(config: EventHandlerConfig) { !event.error && (!event.status || event.status === 'success' || event.status === 'completed'); + const getEventDisplayName = (event: StreamEvent) => { + const typeMap: Record = { + complete: '执行完成', + loop_start: '循环开始', + node_event: '节点事件', + resume: '恢复执行', + start: '开始执行', + }; + return ( + event.node_label || + event.event?.message || + event.event?.content || + event.event?.type || + event.message || + event.content || + typeMap[event.type] || + event.type || + '运行事件' + ); + }; + + const appendObservedStep = ( + event: StreamEvent, + msgId: string, + status: 'completed' | 'failed' | 'running' = 'completed', + ) => { + currentSteps.value.push({ + type: status === 'failed' ? 'error' : 'observation', + content: getEventDisplayName(event), + node_id: event.node_id, + node_type: event.node_type, + ...buildReasoningMeta(event), + params: event.config || event.waiting_config || event.event || event.params, + output: event.outputs || event.output || event.result, + status, + timestamp: new Date().toISOString(), + }); + updateAssistantMessage(msgId, { + reasoning_steps: [...currentSteps.value], + ...buildMessageMeta(event), + }); + }; + /** 处理流式事件 */ const handleStreamEvent = (event: StreamEvent, msgId: string) => { switch (event.type) { @@ -600,7 +643,11 @@ export function useEventHandler(config: EventHandlerConfig) { (s) => s.node_id === event.node_id && s.type === 'parallel_start', ); const branchResults = event.branch_results || {}; - const content = `并行分支完成:${getItemCount(branchResults)} 个分支`; + const nodeSucceeded = isSuccessEvent(event); + const content = nodeSucceeded + ? `并行分支完成:${getItemCount(branchResults)} 个分支` + : `并行分支失败:${getStreamErrorMessage(event)}`; + const nodeStatus = nodeSucceeded ? 'completed' : 'failed'; if (existingIndex === -1) { currentSteps.value.push({ type: 'parallel_complete', @@ -612,7 +659,7 @@ export function useEventHandler(config: EventHandlerConfig) { branch_results: branchResults, total_tokens: event.total_tokens, }, - status: 'completed', + status: nodeStatus, timestamp: new Date().toISOString(), }); } else { @@ -625,7 +672,7 @@ export function useEventHandler(config: EventHandlerConfig) { branch_results: branchResults, total_tokens: event.total_tokens, }, - status: 'completed', + status: nodeStatus, }; } updateAssistantMessage(msgId, { @@ -673,6 +720,8 @@ export function useEventHandler(config: EventHandlerConfig) { }; messages.value.push(newMsg); } + } else if (nodeEvent) { + appendObservedStep(event, msgId); } break; } @@ -719,6 +768,12 @@ export function useEventHandler(config: EventHandlerConfig) { break; } + case 'resume': { + currentRunId.value = event.run_id || currentRunId.value; + appendObservedStep(event, msgId, 'running'); + break; + } + // ========== 对话流交互事件 ========== case 'waiting_input': { waitingForInput.value = true; @@ -752,6 +807,24 @@ export function useEventHandler(config: EventHandlerConfig) { running.value = false; break; } + + default: { + if ( + event.type && + (event.content || + event.message || + event.error || + event.error_message || + event.event) + ) { + appendObservedStep( + event, + msgId, + event.error || event.error_message ? 'failed' : 'completed', + ); + } + break; + } } }; diff --git a/web/apps/web-ele/src/views/ai-platform/workflow-runs/modules/detail-dialog.vue b/web/apps/web-ele/src/views/ai-platform/workflow-runs/modules/detail-dialog.vue index 7d1f78b..dd36df0 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow-runs/modules/detail-dialog.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow-runs/modules/detail-dialog.vue @@ -95,6 +95,115 @@ const selectedNodeLabel = computed(() => { const log = executionLogs.value.find((item) => item.node_id === nodeId); return log?.node_label || log?.node_id || nodeId; }); +const runEventTimeline = computed(() => { + if (!run.value) return []; + + const items: Array<{ + channel: string; + elapsed?: number; + eventType: string; + index?: number; + key: string; + nodeId?: string; + status: string; + summary: string; + timestamp?: string; + title: string; + }> = [ + { + channel: triggerLabel(run.value.trigger_type), + eventType: 'start', + key: 'start', + status: run.value.status === 'running' ? 'running' : 'completed', + summary: previewValue(run.value.inputs), + timestamp: run.value.started_at, + title: '开始运行', + }, + ]; + + let estimatedOffset = 0; + for (const [index, log] of executionLogs.value.entries()) { + const communication = getCommunication(log); + const collaboration = getCollaboration(log); + const eventType = + communication.event || + (log.status === 'waiting' + ? 'waiting_input' + : log.status === 'failed' + ? 'error' + : 'node_complete'); + const branch = + communication.branch_label || + log.branch_label || + log.branch || + collaboration.branch_label; + const subflowName = collaboration.subflow_name || getLogMeta(log).subflow_name; + const targetName = getTargetName(log); + const timestamp = + log.timestamp || + communication.timestamp || + estimateLogTimestamp(estimatedOffset); + estimatedOffset += Number(log.elapsed_time || 0); + const summaryParts = [ + branch ? `分支:${branch}` : '', + subflowName ? `子流程:${subflowName}` : '', + targetName ? `交接:${targetName}` : '', + communication.message ? `摘要:${communication.message}` : '', + getLogError(log) ? `错误:${getLogError(log)}` : '', + ].filter(Boolean); + + items.push({ + channel: communication.channel || collaboration.collaboration_mode || log.node_type || '', + elapsed: log.elapsed_time, + eventType, + index, + key: `${log.node_id || 'event'}-${index}-${eventType}`, + nodeId: log.node_id, + status: normalizeRunEventStatus(eventType, log.status), + summary: summaryParts.join(' · ') || previewValue(log.output), + timestamp, + title: + log.node_label || + getActorName(log) || + getRunEventTypeLabel(eventType), + }); + } + + if (run.value.status === 'waiting') { + items.push({ + channel: 'human_input', + eventType: 'waiting_input', + key: 'waiting', + nodeId: run.value.current_node_id, + status: 'waiting', + summary: previewValue(run.value.waiting_config), + title: '等待用户输入', + }); + } else if (run.value.status === 'failed') { + items.push({ + channel: 'error', + eventType: 'error', + key: 'failed', + status: 'failed', + summary: run.value.error_message || '工作流执行失败', + timestamp: run.value.completed_at, + title: '运行失败', + }); + } else if (run.value.status === 'completed') { + items.push({ + channel: 'workflow', + elapsed: run.value.elapsed_time, + eventType: 'complete', + key: 'complete', + status: 'completed', + summary: previewValue(run.value.outputs), + timestamp: run.value.completed_at, + title: '运行完成', + }); + } + + return items; +}); const agentSummaries = computed(() => { const agents = new Map< string, @@ -380,6 +489,29 @@ function getLogStatusText(status?: string) { return status ? map[status] || status : '-'; } +function getRunEventTypeLabel(type?: string) { + const map: Record = { + complete: '完成', + error: '错误', + llm_chunk: '模型输出', + node_complete: '节点完成', + node_event: '节点事件', + node_start: '节点开始', + parallel_complete: '并行完成', + parallel_start: '并行开始', + start: '开始', + waiting_input: '等待输入', + }; + return type ? map[type] || type : '-'; +} + +function normalizeRunEventStatus(eventType?: string, status?: string) { + if (eventType === 'waiting_input' || status === 'waiting') return 'waiting'; + if (eventType === 'error' || status === 'failed') return 'failed'; + if (eventType === 'start' || status === 'running') return 'running'; + return 'completed'; +} + function previewValue(value: any) { if (value === undefined || value === null || value === '') return '-'; if (typeof value === 'string') return value; @@ -483,6 +615,11 @@ function selectLog(nodeId?: string, index?: number) { } } +function selectRunEvent(item: { index?: number; nodeId?: string }) { + if (!item.nodeId) return; + selectLog(item.nodeId, item.index); +} + defineExpose({ open }); @@ -566,6 +703,58 @@ defineExpose({ open }); +
+
+
+
运行事件
+
+ 串联展示开始、节点、等待、错误、并行和子流程事件 +
+
+ {{ runEventTimeline.length }} +
+
+ +
+
+
@@ -961,6 +1150,67 @@ defineExpose({ open }); padding: 6px 10px; } +.workflow-run-detail-dialog .run-event-list { + display: grid; + grid-auto-columns: minmax(240px, 320px); + grid-auto-flow: column; + gap: 8px; + overflow-x: auto; + padding-bottom: 2px; +} + +.workflow-run-detail-dialog .run-event-item { + display: grid; + grid-template-columns: 8px minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + width: 100%; + min-height: 58px; + border: 1px solid var(--el-border-color-lighter); + border-radius: 8px; + background: var(--el-fill-color-blank); + padding: 8px 10px; + text-align: left; +} + +.workflow-run-detail-dialog .run-event-item.is-clickable { + cursor: pointer; +} + +.workflow-run-detail-dialog .run-event-item.is-clickable:hover { + border-color: var(--el-color-primary-light-5); + background: var(--el-fill-color-light); +} + +.workflow-run-detail-dialog .run-event-item.is-active { + border-color: var(--el-color-primary); + background: var(--el-color-primary-light-9); + box-shadow: 0 0 0 2px var(--el-color-primary-light-7); +} + +.workflow-run-detail-dialog .run-event-dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--el-text-color-placeholder); +} + +.workflow-run-detail-dialog .run-event-dot.is-success { + background: var(--el-color-success); +} + +.workflow-run-detail-dialog .run-event-dot.is-danger { + background: var(--el-color-danger); +} + +.workflow-run-detail-dialog .run-event-dot.is-warning { + background: var(--el-color-warning); +} + +.workflow-run-detail-dialog .run-event-dot.is-primary { + background: var(--el-color-primary); +} + .workflow-run-detail-dialog .collaboration-timeline { display: flex; flex-direction: column; @@ -1143,6 +1393,12 @@ defineExpose({ open }); grid-template-columns: 1fr; } + .workflow-run-detail-dialog .run-event-list { + grid-auto-flow: row; + grid-template-columns: 1fr; + overflow-x: visible; + } + .workflow-run-detail-dialog .collaboration-flow { flex-wrap: wrap; overflow-x: visible;