Phase G3: ACP Gateway + DAG Multi-Agent - acp_gateway.py (Agent communication layer, 350L), workflow_admin.py upgraded (async parallel execution + agent_node), Workflow.tsx updated (agent node + real-time streaming + WebSocket)
This commit is contained in:
1 parent
9b3e15ace6
commit
d2e3147fb8
3 files changed
+1073
-88
No files matched your search
@@ -47,6 +47,7 @@ const NODE_COLORS: Record<string, string> = {
|
||||
condition: "#BA7517",
|
||||
http: "#D4537E",
|
||||
code: "#5F5E5A",
|
||||
agent: "#7C3AED",
|
||||
end: "#A32D2D",
|
||||
};
|
||||
|
||||
@@ -58,6 +59,7 @@ const NODE_LABELS: Record<string, string> = {
|
||||
condition: "条件分支",
|
||||
http: "HTTP 请求",
|
||||
code: "代码执行",
|
||||
agent: "Agent 节点",
|
||||
end: "结束",
|
||||
};
|
||||
|
||||
@@ -75,6 +77,8 @@ export default function Workflow() {
|
||||
const [executeResult, setExecuteResult] = useState<any>(null);
|
||||
const [executeInput, setExecuteInput] = useState("");
|
||||
const [showExecute, setShowExecute] = useState(false);
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [nodeStatus, setNodeStatus] = useState<Record<string, string>>({});
|
||||
const [stats, setStats] = useState({ total: 0, draft: 0, published: 0, archived: 0 });
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
@@ -278,19 +282,95 @@ export default function Workflow() {
|
||||
setSelectedNode(updatedNodes.find(n => n.id === selectedNode.id) || null);
|
||||
};
|
||||
|
||||
// Execute workflow
|
||||
// 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) {
|
||||
if (res.code === 0 && !result) {
|
||||
setExecuteResult(res.data);
|
||||
} else {
|
||||
alert("执行失败: " + (res.message || "未知错误"));
|
||||
}
|
||||
} catch (e) { console.error(e); alert("执行失败"); }
|
||||
} 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)
|
||||
@@ -312,7 +392,7 @@ export default function Workflow() {
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "24px" }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: "20px", color: "#fff" }}>Agent 可视化编排</h2>
|
||||
<p style={{ margin: "4px 0 0", color: "#888", fontSize: "13px" }}>拖拽式 DAG 工作流编排引擎 · 支持LLM/知识库/Skill/条件分支/HTTP/代码节点</p>
|
||||
<p style={{ margin: "4px 0 0", color: "#888", fontSize: "13px" }}>拖拽式 DAG 工作流编排引擎 · LLM/知识库/Skill/条件/HTTP/代码/Agent 节点 · 并行多 Agent 协作</p>
|
||||
</div>
|
||||
<button onClick={handleCreate} style={btnPrimary}>+ 新建工作流</button>
|
||||
</div>
|
||||
@@ -408,8 +488,15 @@ export default function Workflow() {
|
||||
style={{ width: "100%", background: "#0d0d1a", border: "1px solid #333", borderRadius: "6px", padding: "8px", color: "#fff", fontSize: "13px", minHeight: "40px", resize: "vertical" }}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={handleExecute} style={btnPrimary}>执行工作流</button>
|
||||
<button onClick={handleExecute} style={{ ...btnPrimary, opacity: executing ? 0.6 : 1 }} disabled={executing}>
|
||||
{executing ? "执行中..." : "执行工作流"}
|
||||
</button>
|
||||
</div>
|
||||
{executing && (
|
||||
<div style={{ marginTop: "8px", fontSize: "12px", color: "#534AB7" }}>
|
||||
⏳ 工作流执行中...
|
||||
</div>
|
||||
)}
|
||||
{executeResult && (
|
||||
<div style={{ marginTop: "12px" }}>
|
||||
<div style={{ fontSize: "12px", color: "#888", marginBottom: "4px" }}>执行日志:</div>
|
||||
@@ -500,7 +587,10 @@ export default function Workflow() {
|
||||
{/* Nodes */}
|
||||
{currentWf.nodes.map(node => {
|
||||
const isSelected = selectedNode?.id === node.id;
|
||||
const color = NODE_COLORS[node.type] || "#5F5E5A";
|
||||
const status = nodeStatus[node.id];
|
||||
const baseColor = NODE_COLORS[node.type] || "#5F5E5A";
|
||||
const color = status === "running" ? "#58a6ff" : status === "completed" ? "#3fb950" : status === "failed" ? "#f85149" : baseColor;
|
||||
const borderWidth = isSelected || status ? 2 : 1;
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
@@ -513,7 +603,7 @@ export default function Workflow() {
|
||||
width: "90px",
|
||||
minHeight: "50px",
|
||||
background: "#1a1a2e",
|
||||
border: `2px solid ${isSelected ? color : "#333"}`,
|
||||
border: `${borderWidth}px solid ${color}`,
|
||||
borderRadius: "8px",
|
||||
cursor: draggingNode === node.id ? "grabbing" : "grab",
|
||||
padding: "8px",
|
||||
|
||||
Reference in new issue
Block a user