前端 + AI 进阶 Day 13:工作流执行监控

前端 + AI 进阶学习路线|Week 11-12:智能工作流前端

Day 13:工作流执行监控

学习时间:2026年1月6日(星期二)
关键词:工作流执行、DAG 遍历、实时状态、进度条、日志面板、执行引擎


📁 项目文件结构

day13-workflow-execution/
├── src/
│   ├── components/
│   │   ├── WorkflowCanvas.jsx        # 复用 Day 12 画布(带状态)
│   │   ├── ExecutionPanel.jsx        # 执行控制 + 日志面板
│   │   └── NodeTypes/                # 复用 Day 12 节点(带状态样式)
│   │       ├── AINode.jsx
│   │       ├── InputNode.jsx
│   │       └── OutputNode.jsx
│   ├── lib/
│   │   └── workflowEngine.js         # 工作流执行引擎
│   └── App.jsx                       # 主应用集成
└── public/

✅ 本日核心:让可视化工作流“动起来”——实时执行 + 状态监控


🎯 今日学习目标

  1. 实现 工作流执行引擎(基于 DAG 拓扑排序)
  2. 显示 节点实时状态(待运行 / 运行中 / 成功 / 失败)
  3. 构建 执行日志面板整体进度条
  4. 支持 手动触发执行结果查看

💡 为什么需要执行监控?

可视化只是第一步,用户更关心:

  • “我的工作流跑起来了吗?”
  • “哪一步卡住了?为什么失败?”
  • “需要多久才能完成?”

执行可视化 是工作流系统的核心价值,让自动化过程透明可控


📚 核心设计思路

功能 实现方式
DAG 遍历 拓扑排序 + 并行/串行执行策略
节点状态 扩展节点数据:{ status: 'idle' | 'running' | 'success' | 'error' }
日志收集 执行过程中收集 console.log + 自定义日志
进度计算 (已完成节点数 / 总节点数) * 100

⚠️ 注意:前端执行适合轻量任务(如 API 调用),重计算应交给后端


🔧 动手实践:构建工作流执行监控系统

步骤 1:创建项目并复用 Day 12 组件

npx create-react-app day13-workflow-execution
cd day13-workflow-execution
npm install reactflow
# 复制 Day 12 的 components/ 和 lib/

步骤 2:扩展现有节点以支持状态

// src/components/NodeTypes/AINode.jsx(更新版)
import { Handle, Position } from 'reactflow';

const statusColors = {
  idle: '#52c41a',
  running: '#13c2c2',
  success: '#52c41a',
  error: '#ff4d4f'
};

const statusLabels = {
  idle: '● 待运行',
  running: '● 运行中',
  success: '✅ 成功',
  error: '❌ 失败'
};

export default function AINode({ data }) {
  const status = data.status || 'idle';
  const color = statusColors[status];
  
  return (
    <div style={{
      background: status === 'error' ? '#fff2f0' : '#f6ffed',
      border: `2px solid ${color}`,
      borderRadius: '8px',
      padding: '12px 16px',
      minWidth: '160px',
      boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
      position: 'relative'
    }}>
      <div style={{ 
        fontWeight: '600', 
        color: color,
        marginBottom: '4px',
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center'
      }}>
        🤖 AI 模型
        <span style={{ fontSize: '12px', fontWeight: 'normal' }}>
          {statusLabels[status]}
        </span>
      </div>
      <div style={{ fontSize: '12px', color: '#666', wordBreak: 'break-word' }}>
        {data.label || 'LLM 分析'}
      </div>
      <Handle type="target" position={Position.Left} />
      <Handle type="source" position={Position.Right} />
    </div>
  );
}

💡 InputNode.jsxOutputNode.jsx 按相同逻辑更新 status 样式

步骤 3:创建工作流执行引擎

// src/lib/workflowEngine.js
/**
 * 模拟工作流执行(真实场景替换为 API 调用)
 * @param {Object} node - 节点对象
 * @param {Object} input - 输入数据
 * @returns {Promise<Object>} - 输出数据
 */
const executeNode = async (node, input) => {
  await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000)); // 模拟耗时

  // 模拟不同节点行为
  if (node.type === 'input') {
    return { data: input || '用户输入内容' };
  }
  
  if (node.type === 'ai') {
    // 模拟 AI 调用
    const responses = [
      '✅ AI 分析完成:检测到 3 个关键实体',
      '✅ AI 生成摘要:文本长度 120 字',
      '❌ AI 调用失败:模型超时',
      '✅ AI 翻译完成:中 → 英'
    ];
    const response = responses[Math.floor(Math.random() * responses.length)];
    if (response.includes('❌')) {
      throw new Error('模型调用失败');
    }
    return { data: response };
  }
  
  if (node.type === 'output') {
    return { data: `最终输出: ${input}` };
  }
  
  return { data: '处理完成' };
};

/**
 * 工作流执行器
 * @param {Array} nodes - 节点列表
 * @param {Array} edges - 边列表
 * @param {function} onUpdateNode - (nodeId, status, output) => void
 * @param {function} onLog - (message) => void
 */
export const executeWorkflow = async ({ nodes, edges, onUpdateNode, onLog }) => {
  // 构建邻接表和入度表(用于拓扑排序)
  const graph = new Map();
  const inDegree = new Map();
  
  nodes.forEach(node => {
    graph.set(node.id, []);
    inDegree.set(node.id, 0);
  });
  
  edges.forEach(edge => {
    graph.get(edge.source).push(edge.target);
    inDegree.set(edge.target, inDegree.get(edge.target) + 1);
  });
  
  // 拓扑排序(Kahn 算法)
  const queue = [];
  const results = new Map(); // nodeId -> output
  
  // 初始化:入度为 0 的节点入队
  nodes.forEach(node => {
    if (inDegree.get(node.id) === 0) {
      queue.push(node.id);
    }
  });
  
  // 执行队列
  while (queue.length > 0) {
    // 并行执行当前层级所有节点
    const currentLevel = [...queue];
    queue.length = 0;
    
    const promises = currentLevel.map(async (nodeId) => {
      const node = nodes.find(n => n.id === nodeId);
      onUpdateNode(nodeId, 'running');
      onLog(`▶ 开始执行节点: ${node.data?.label || nodeId}`);
      
      try {
        // 获取输入(来自前驱节点)
        const inputEdges = edges.filter(e => e.target === nodeId);
        let input = null;
        if (inputEdges.length > 0) {
          // 简化:取第一个前驱的输出
          const sourceId = inputEdges[0].source;
          input = results.get(sourceId)?.data;
        }
        
        const output = await executeNode(node, input);
        results.set(nodeId, output);
        onUpdateNode(nodeId, 'success');
        onLog(`✅ 节点 ${nodeId} 执行成功`);
      } catch (error) {
        onUpdateNode(nodeId, 'error');
        onLog(`❌ 节点 ${nodeId} 执行失败: ${error.message}`);
        throw error; // 中止整个工作流
      }
      
      // 更新后继节点入度
      const neighbors = graph.get(nodeId);
      neighbors?.forEach(neighbor => {
        const newDegree = inDegree.get(neighbor) - 1;
        inDegree.set(neighbor, newDegree);
        if (newDegree === 0) {
          queue.push(neighbor);
        }
      });
    });
    
    // 等待当前层级全部完成
    await Promise.all(promises);
  }
  
  return results;
};

步骤 4:创建执行控制面板

// src/components/ExecutionPanel.jsx
import { useState, useRef } from 'react';
import { executeWorkflow } from '../lib/workflowEngine';

export default function ExecutionPanel({ 
  nodes, 
  edges, 
  onUpdateNode,
  onResetNodes 
}) {
  const [isExecuting, setIsExecuting] = useState(false);
  const [logs, setLogs] = useState([]);
  const [progress, setProgress] = useState(0);
  const logContainerRef = useRef(null);

  const handleExecute = async () => {
    if (isExecuting) return;
    
    setIsExecuting(true);
    setLogs([]);
    setProgress(0);
    onResetNodes(); // 重置所有节点状态
    
    try {
      await executeWorkflow({
        nodes,
        edges,
        onUpdateNode: (nodeId, status, output) => {
          onUpdateNode(nodeId, status, output);
          
          // 更新进度
          const completedNodes = nodes.filter(n => 
            ['success', 'error'].includes(n.data?.status)
          ).length;
          setProgress(Math.round((completedNodes / nodes.length) * 100));
        },
        onLog: (message) => {
          setLogs(prev => [...prev, `${new Date().toLocaleTimeString()} ${message}`]);
          // 自动滚动到底部
          setTimeout(() => {
            if (logContainerRef.current) {
              logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
            }
          }, 10);
        }
      });
      
      setLogs(prev => [...prev, '🎉 工作流执行完成!']);
    } catch (error) {
      setLogs(prev => [...prev, `💥 工作流执行失败: ${error.message}`]);
    } finally {
      setIsExecuting(false);
    }
  };

  const clearLogs = () => {
    setLogs([]);
  };

  return (
    <div style={{
      padding: '16px',
      borderTop: '1px solid #e8e8e8',
      backgroundColor: '#fff',
      width: '100%',
      display: 'flex',
      flexDirection: 'column',
      gap: '12px'
    }}>
      {/* 执行控制 */}
      <div style={{ display: 'flex', gap: '12px' }}>
        <button
          onClick={handleExecute}
          disabled={isExecuting}
          style={{
            padding: '8px 16px',
            backgroundColor: isExecuting ? '#b0b0b0' : '#52c41a',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: isExecuting ? 'not-allowed' : 'pointer',
            fontSize: '14px',
            fontWeight: '500'
          }}
        >
          {isExecuting ? '🔄 执行中...' : '▶ 执行工作流'}
        </button>
        
        <button
          onClick={onResetNodes}
          disabled={isExecuting}
          style={{
            padding: '8px 16px',
            backgroundColor: '#f0f0f0',
            border: '1px solid #d9d9d9',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '14px'
          }}
        >
          🔄 重置
        </button>
      </div>

      {/* 进度条 */}
      <div>
        <div style={{ 
          display: 'flex', 
          justifyContent: 'space-between', 
          fontSize: '12px', 
          color: '#666',
          marginBottom: '4px'
        }}>
          <span>执行进度</span>
          <span>{progress}%</span>
        </div>
        <div style={{
          height: '6px',
          backgroundColor: '#f0f0f0',
          borderRadius: '3px',
          overflow: 'hidden'
        }}>
          <div
            style={{
              height: '100%',
              width: `${progress}%`,
              backgroundColor: progress === 100 ? '#52c41a' : '#1890ff',
              transition: 'width 0.3s ease'
            }}
          />
        </div>
      </div>

      {/* 日志面板 */}
      <div>
        <div style={{ 
          display: 'flex', 
          justifyContent: 'space-between', 
          alignItems: 'center',
          marginBottom: '8px'
        }}>
          <h4 style={{ margin: 0, fontSize: '14px' }}>📝 执行日志</h4>
          <button
            onClick={clearLogs}
            style={{
              background: 'none',
              border: 'none',
              color: '#1890ff',
              fontSize: '12px',
              cursor: 'pointer'
            }}
          >
            清空
          </button>
        </div>
        <div
          ref={logContainerRef}
          style={{
            height: '120px',
            padding: '8px',
            backgroundColor: '#f9f9f9',
            borderRadius: '4px',
            border: '1px solid #e8e8e8',
            overflowY: 'auto',
            fontSize: '12px',
            fontFamily: 'monospace'
          }}
        >
          {logs.map((log, idx) => (
            <div key={idx} style={{ 
              marginBottom: '4px',
              whiteSpace: 'pre-wrap'
            }}>
              {log}
            </div>
          ))}
          {logs.length === 0 && (
            <div style={{ color: '#888', fontStyle: 'italic' }}>
              点击“执行工作流”查看日志
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

步骤 5:在 App.jsx 中集成执行逻辑

// src/App.jsx
import React, { useState } from 'react';
import WorkflowCanvas from './components/WorkflowCanvas';
import ControlsPanel from './components/ControlsPanel';
import ExecutionPanel from './components/ExecutionPanel';

function App() {
  const [nodes, setNodes] = useState([]);
  const [edges, setEdges] = useState([]);

  // 更新单个节点状态
  const updateNodeStatus = (nodeId, status, output) => {
    setNodes(prev => 
      prev.map(node => 
        node.id === nodeId 
          ? { 
              ...node, 
              data: { 
                ...node.data, 
                status, 
                output 
              } 
            } 
          : node
      )
    );
  };

  // 重置所有节点状态
  const resetNodes = () => {
    setNodes(prev => 
      prev.map(node => ({
        ...node,
        data: {
          ...node.data,
          status: 'idle',
          output: null
        }
      }))
    );
  };

  return (
    <div style={{ 
      fontFamily: 'Inter, -apple-system, sans-serif',
      width: '100vw',
      height: '100vh',
      display: 'flex',
      flexDirection: 'column'
    }}>
      <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
        <ControlsPanel />
        <WorkflowCanvas 
          nodes={nodes}
          edges={edges}
          onNodesChange={setNodes}
          onEdgesChange={setEdges}
        />
      </div>
      <ExecutionPanel 
        nodes={nodes}
        edges={edges}
        onUpdateNode={updateNodeStatus}
        onResetNodes={resetNodes}
      />
    </div>
  );
}

export default App;

💡 WorkflowCanvas.jsx 需更新以接收 nodes/edges 作为 props(而非内部状态)


✅ 效果验证

  • ✅ 点击“▶ 执行工作流” → 节点状态从“待运行” → “运行中” → “成功/失败”
  • ✅ 进度条实时更新(0% → 100%)
  • ✅ 日志面板显示执行时间戳 + 详细信息
  • ✅ 失败节点显示红色边框 + “❌ 失败”标签
  • ✅ 点击“🔄 重置” → 所有节点恢复“待运行”状态

🤔 思考与延伸

  1. 真实 AI 集成:如何替换 executeNode 为真实 Ollama/OpenAI 调用?
    → 在 executeNode 中调用 streamAIResponse(参考 Day 4)

  2. 并行优化:如何最大化并行度?
    → 拓扑排序天然支持同层级并行,可限制并发数

  3. 结果导出:如何保存最终输出?
    → 在执行完成后,将 results 存入 localStorage 或下载 JSON

💡 为 Day 14 做准备:执行结果可作为“工作流模板”的输入示例


📅 明日预告

Day 14:工作流模板市场

  • 实现工作流模板保存/导入
  • 构建模板分享与发现界面
  • 支持一键应用模板到画布

✍️ 小结

今天,我们让静态的工作流“活”了起来!通过执行引擎与实时监控,用户可清晰看到每一步的状态与日志,真正掌握自动化流程。可观测性,是复杂系统可用性的基石。

💬 实践提示:真实项目中,建议将执行日志按节点分组,并支持筛选/搜索。欢迎分享你的工作流执行监控界面!

posted @ 2025-12-31 10:01  XiaoZhengTou  阅读(0)  评论(0)    收藏  举报