import React, { useState, useEffect, useCallback, useRef } from "react"; import { useNavigate } from "react-router-dom"; interface WorkflowNode { id: string; type: string; position: { x: number; y: number }; data: { label: string; config: Record }; } interface WorkflowEdge { id: string; source: string; target: string; sourceHandle?: string | null; targetHandle?: string | null; } interface Workflow { id: string; name: string; description: string; agent_id: string | null; nodes: WorkflowNode[]; edges: WorkflowEdge[]; variables: Record; status: string; version: number; created_at: string; updated_at: string; } interface NodeTemplate { type: string; label: string; icon: string; color: string; description: string; config_schema: Record; } const NODE_COLORS: Record = { start: "#3B6D11", llm: "#534AB7", knowledge: "#185FA5", skill: "#0F6E56", condition: "#BA7517", http: "#D4537E", code: "#5F5E5A", agent: "#7C3AED", end: "#A32D2D", }; const NODE_LABELS: Record = { start: "开始", llm: "LLM 调用", knowledge: "知识库检索", skill: "Skill 调用", condition: "条件分支", http: "HTTP 请求", code: "代码执行", agent: "Agent 节点", end: "结束", }; export default function Workflow() { const navigate = useNavigate(); const [workflows, setWorkflows] = useState([]); const [loading, setLoading] = useState(true); const [view, setView] = useState<"list" | "editor">("list"); const [currentWf, setCurrentWf] = useState(null); const [templates, setTemplates] = useState([]); const [options, setOptions] = useState>({}); const [selectedNode, setSelectedNode] = useState(null); const [draggingNode, setDraggingNode] = useState(null); const [connectingFrom, setConnectingFrom] = useState(null); const [executeResult, setExecuteResult] = useState(null); const [executeInput, setExecuteInput] = useState(""); const [showExecute, setShowExecute] = useState(false); const [executing, setExecuting] = useState(false); const [nodeStatus, setNodeStatus] = useState>({}); const [stats, setStats] = useState({ total: 0, draft: 0, published: 0, archived: 0 }); const canvasRef = useRef(null); const dragOffset = useRef({ x: 0, y: 0 }); const api = (window as any).api; // Load workflows const loadWorkflows = useCallback(async () => { setLoading(true); try { const res = await api.get("/admin/workflows/"); if (res.code === 0) setWorkflows(res.data.items || []); } catch (e) { console.error(e); } try { const s = await api.get("/admin/workflows/stats"); if (s.code === 0) setStats(s.data); } catch (e) { console.error(e); } setLoading(false); }, []); // Load templates const loadTemplates = useCallback(async () => { try { const res = await api.get("/admin/workflows/templates"); if (res.code === 0) { setTemplates(Object.values(res.data.node_templates || {})); setOptions(res.data.options || {}); } } catch (e) { console.error(e); } }, []); useEffect(() => { loadWorkflows(); loadTemplates(); }, []); // Create new workflow const handleCreate = async () => { const name = prompt("工作流名称:", "新工作流"); if (!name) return; try { const res = await api.post("/admin/workflows/", { name, description: "", nodes: [ { id: "start-1", type: "start", position: { x: 100, y: 200 }, data: { label: "开始", config: { input_variable: "input" } } }, { id: "end-1", type: "end", position: { x: 500, y: 200 }, data: { label: "结束", config: { output_variable: "output" } } }, ], edges: [], }); if (res.code === 0) { loadWorkflows(); handleOpenEditor(res.data); } } catch (e) { console.error(e); } }; // Open editor const handleOpenEditor = (wf: Workflow) => { setCurrentWf(wf); setSelectedNode(null); setView("editor"); }; // Save workflow const handleSave = async () => { if (!currentWf) return; try { const res = await api.put(`/admin/workflows/${currentWf.id}`, { name: currentWf.name, description: currentWf.description, nodes: currentWf.nodes, edges: currentWf.edges, variables: currentWf.variables, }); if (res.code === 0) { setCurrentWf(res.data); alert("保存成功"); } } catch (e) { console.error(e); } }; // Delete workflow const handleDelete = async (id: string) => { if (!confirm("确认删除此工作流?")) return; try { await api.del(`/admin/workflows/${id}`); loadWorkflows(); } catch (e) { console.error(e); } }; // Toggle publish const handleToggle = async (wf: Workflow) => { const isActive = wf.status !== "published"; try { await api.put(`/admin/workflows/${wf.id}/toggle?is_active=${isActive}`); loadWorkflows(); } catch (e) { console.error(e); } }; // Add node to canvas const handleAddNode = (template: NodeTemplate) => { if (!currentWf) return; const id = `${template.type}-${Date.now()}`; const newNode: WorkflowNode = { id, type: template.type, position: { x: 250 + Math.random() * 100, y: 150 + Math.random() * 100 }, data: { label: template.label, config: Object.fromEntries( Object.entries(template.config_schema).map(([k, v]: [string, any]) => [k, v.default ?? ""]) ), }, }; setCurrentWf({ ...currentWf, nodes: [...currentWf.nodes, newNode] }); }; // Node drag const handleNodeMouseDown = (e: React.MouseEvent, nodeId: string) => { e.stopPropagation(); const node = currentWf!.nodes.find(n => n.id === nodeId); if (!node || !canvasRef.current) return; const rect = canvasRef.current.getBoundingClientRect(); dragOffset.current = { x: e.clientX - rect.left - node.position.x, y: e.clientY - rect.top - node.position.y, }; setDraggingNode(nodeId); setSelectedNode(node); }; const handleCanvasMouseMove = (e: React.MouseEvent) => { if (!draggingNode || !currentWf || !canvasRef.current) return; const rect = canvasRef.current.getBoundingClientRect(); const x = e.clientX - rect.left - dragOffset.current.x; const y = e.clientY - rect.top - dragOffset.current.y; setCurrentWf({ ...currentWf, nodes: currentWf.nodes.map(n => n.id === draggingNode ? { ...n, position: { x, y } } : n), }); }; const handleCanvasMouseUp = () => { setDraggingNode(null); }; // Canvas click (deselect / cancel connecting) const handleCanvasClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { setSelectedNode(null); setConnectingFrom(null); } }; // Start connecting from a node const handleStartConnect = (e: React.MouseEvent, nodeId: string) => { e.stopPropagation(); setConnectingFrom(nodeId); }; // Complete connection to a node const handleEndConnect = (e: React.MouseEvent, nodeId: string) => { e.stopPropagation(); if (!connectingFrom || !currentWf) return; if (connectingFrom === nodeId) { setConnectingFrom(null); return; } // Check if edge already exists const exists = currentWf.edges.some(e => e.source === connectingFrom && e.target === nodeId); if (exists) { setConnectingFrom(null); return; } const newEdge: WorkflowEdge = { id: `e-${Date.now()}`, source: connectingFrom, target: nodeId, }; setCurrentWf({ ...currentWf, edges: [...currentWf.edges, newEdge] }); setConnectingFrom(null); }; // Delete edge const handleDeleteEdge = (edgeId: string) => { if (!currentWf) return; setCurrentWf({ ...currentWf, edges: currentWf.edges.filter(e => e.id !== edgeId) }); }; // Delete selected node const handleDeleteNode = () => { if (!selectedNode || !currentWf) return; const nodeId = selectedNode.id; setCurrentWf({ ...currentWf, nodes: currentWf.nodes.filter(n => n.id !== nodeId), edges: currentWf.edges.filter(e => e.source !== nodeId && e.target !== nodeId), }); setSelectedNode(null); }; // Update node config const handleConfigChange = (key: string, value: any) => { if (!selectedNode || !currentWf) return; const updatedNodes = currentWf.nodes.map(n => n.id === selectedNode.id ? { ...n, data: { ...n.data, label: n.data.label, config: { ...n.data.config, [key]: value } } } : n ); setCurrentWf({ ...currentWf, nodes: updatedNodes }); setSelectedNode(updatedNodes.find(n => n.id === selectedNode.id) || null); }; // Execute workflow (supports streaming via WebSocket when available) const handleExecute = async () => { if (!currentWf) return; setExecuting(true); setExecuteResult(null); setNodeStatus({}); // Highlight running nodes in real-time const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsHost = window.location.host; const wsUrl = `${wsProtocol}//${wsHost}/app/api/admin/workflows/${currentWf.id}/execute-stream`; let ws: WebSocket | null = null; let result: any = null; try { // Connect WebSocket ws = new WebSocket(wsUrl); ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === "node_start") { setNodeStatus(prev => ({ ...prev, [msg.node_id]: "running" })); // Add to log setExecuteResult((prev: any) => ({ ...prev, log: [...(prev?.log || []), { node_id: msg.node_id, node_label: msg.node_label, node_type: msg.node_type, success: null, }], })); } else if (msg.type === "node_complete") { setNodeStatus(prev => ({ ...prev, [msg.node_id]: "completed" })); // Update log entry setExecuteResult((prev: any) => { const log = (prev?.log || []).map((l: any) => l.node_id === msg.node_id ? { ...l, success: true, output_preview: msg.output_preview } : l ); return { ...prev, log }; }); } else if (msg.type === "node_error") { setNodeStatus(prev => ({ ...prev, [msg.node_id]: "failed" })); setExecuteResult((prev: any) => { const log = (prev?.log || []).map((l: any) => l.node_id === msg.node_id ? { ...l, success: false, error: msg.error } : l ); return { ...prev, log }; }); } else if (msg.type === "workflow_complete") { result = msg.data; setExecuteResult(msg.data); } else if (msg.type === "workflow_error") { result = msg.data; setExecuteResult(msg.data); } } catch (e) { /* ignore parse errors */ } }; ws.onopen = () => { ws?.send(JSON.stringify({ input_text: executeInput, token: (window as any).api?.getToken?.() || "", })); }; // Also call REST API as fallback const res = await api.post(`/admin/workflows/${currentWf.id}/execute`, { input_text: executeInput, }); if (res.code === 0 && !result) { setExecuteResult(res.data); } } catch (e) { console.error(e); // Try REST API fallback try { const res = await api.post(`/admin/workflows/${currentWf.id}/execute`, { input_text: executeInput, }); if (res.code === 0) setExecuteResult(res.data); } catch (e2) { console.error(e2); } } finally { setExecuting(false); setTimeout(() => { if (ws?.readyState === WebSocket.OPEN) ws.close(); }, 1000); } }; // SVG edge path (bezier curve) const getEdgePath = (src: WorkflowNode, tgt: WorkflowNode) => { const x1 = src.position.x + 90; const y1 = src.position.y + 25; const x2 = tgt.position.x; const y2 = tgt.position.y + 25; const dx = Math.abs(x2 - x1) * 0.5; return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`; }; // ============================================================ // LIST VIEW // ============================================================ if (view === "list") { return (

Agent 可视化编排

拖拽式 DAG 工作流编排引擎 · LLM/知识库/Skill/条件/HTTP/代码/Agent 节点 · 并行多 Agent 协作

{/* Stats */}
{[ { label: "总工作流", value: stats.total, color: "#534AB7" }, { label: "草稿", value: stats.draft, color: "#BA7517" }, { label: "已发布", value: stats.published, color: "#3B6D11" }, { label: "已归档", value: stats.archived, color: "#5F5E5A" }, ].map(s => (
{s.value}
{s.label}
))}
{/* Workflow list */}
{["名称", "状态", "节点数", "版本", "更新时间", "操作"].map(h => ( ))} {loading ? ( ) : workflows.length === 0 ? ( ) : workflows.map(wf => ( ))}
{h}
加载中...
暂无工作流,点击"新建工作流"开始
{wf.name}
{wf.description || "—"}
{wf.status === "published" ? "已发布" : wf.status === "archived" ? "已归档" : "草稿"} {wf.nodes?.length || 0} v{wf.version} {wf.updated_at?.slice(0, 19).replace("T", " ") || "—"}
); } // ============================================================ // EDITOR VIEW // ============================================================ if (!currentWf) return null; return (
{/* Toolbar */}
setCurrentWf({ ...currentWf, name: e.target.value })} style={{ background: "transparent", border: "1px solid rgba(0,0,0,0.08)", borderRadius: "6px", padding: "4px 12px", color: "#1A1F36", fontSize: "14px", width: "200px" }} /> v{currentWf.version} · {currentWf.nodes.length} 节点 · {currentWf.edges.length} 连线
{selectedNode && }
{/* Execute panel */} {showExecute && (