import { useEffect, useMemo, useState } from 'react'; import { ApartmentOutlined, ClockCircleOutlined, PlayCircleOutlined, PlusOutlined, ThunderboltOutlined } from '@ant-design/icons'; import { Button, Card, Drawer, Empty, Form, Input, Modal, Select, Space, Switch, Table, Tag, Tooltip, Typography, App as AntApp, Popconfirm, Tabs } from 'antd'; import { Workflow, WorkflowAPI, WorkflowGraph, WorkflowNode, WorkflowNodeType, WorkflowRun, WorkflowRunDetail, streamWorkflowRun } from '../api'; const { Text, Paragraph } = Typography; const NODE_TYPE_LABEL: Record = { agent: '🤖 Agent', skill: '🛠️ Skill', http: '🌐 HTTP', transform: '🔧 Transform', branch: '🔀 Branch' }; const STATUS_COLOR: Record = { running: 'blue', success: 'green', failed: 'red', aborted: 'orange', skipped: 'default' }; const SAMPLE_GRAPH: WorkflowGraph = { entry: 'fetch', variables: { topic: 'Go 1.23 新特性' }, nodes: [ { id: 'fetch', type: 'agent', name: '搜集资料', config: { agentId: '', prompt: '请围绕主题"{{vars.topic}}"列出 5 条最近的关键信息,要点形式。' }, next: 'summarize' }, { id: 'summarize', type: 'agent', name: '总结成稿', config: { agentId: '', prompt: '基于以下要点写一段 200 字日报:\n{{steps.fetch.output.text}}' }, next: '' } ] }; export default function WorkflowsPage() { const { message, modal } = AntApp.useApp(); const [list, setList] = useState([]); const [loading, setLoading] = useState(false); const [editorOpen, setEditorOpen] = useState(false); const [editing, setEditing] = useState(null); const [runsOpen, setRunsOpen] = useState(false); const [runsFor, setRunsFor] = useState(null); const load = async () => { setLoading(true); try { const data = await WorkflowAPI.list(); setList(data); } catch (e: any) { message.error('加载失败:' + (e?.message ?? e)); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const onCreate = () => { setEditing({ id: '', name: '新工作流', description: '', graph: SAMPLE_GRAPH, scheduleCron: '', scheduleEnabled: false, enabled: true, lastRunAt: 0, runCount: 0, createdAt: 0, updatedAt: 0 }); setEditorOpen(true); }; const onEdit = (w: Workflow) => { setEditing(w); setEditorOpen(true); }; const onDelete = async (w: Workflow) => { await WorkflowAPI.remove(w.id); message.success('已删除'); load(); }; return (
自动化编排中心

工作流编排

让多个 Agent、技能、HTTP 请求与数据转换连成一条可运行的自动化链路。这里更像一个流程画廊,而不是传统表格后台。
{[ { label: '工作流数量', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' }, { label: '启用中', value: list.filter((item) => item.enabled).length, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' }, { label: '定时运行', value: list.filter((item) => item.scheduleEnabled && item.scheduleCron).length, tone: 'rgba(249, 115, 22, 0.10)', color: 'var(--color-warning)' } ].map((item) => (
{item.label}
{item.value} 实时状态
))}
{loading ? null : list.length === 0 ? (
) : (
{list.map((r) => (
{r.name}
{r.description || '还没有补充描述,可以说明这个流程负责什么自动化任务。'}
{r.enabled ? '已启用' : '已停用'}
节点数
{r.graph?.nodes?.length ?? 0}
运行次数
{r.runCount}
触发方式
{r.scheduleEnabled && r.scheduleCron ? '定时' : '手动'}
调度与运行
{r.scheduleEnabled && r.scheduleCron ? ( cron: {r.scheduleCron} ) : ( 手动触发 )}
上次运行 {r.lastRunAt ? new Date(r.lastRunAt).toLocaleString() : '尚未运行'}
onDelete(r)}>
))}
)} {editing && ( setEditorOpen(false)} onSaved={() => { setEditorOpen(false); load(); }} /> )} {runsFor && ( setRunsOpen(false)} /> )}
); } // ================== 编辑器 ================== function WorkflowEditor({ open, workflow, onClose, onSaved }: { open: boolean; workflow: Workflow; onClose: () => void; onSaved: () => void; }) { const { message } = AntApp.useApp(); const [form] = Form.useForm(); const [graphText, setGraphText] = useState(JSON.stringify(workflow.graph, null, 2)); const [saving, setSaving] = useState(false); useEffect(() => { form.setFieldsValue({ name: workflow.name, description: workflow.description, scheduleCron: workflow.scheduleCron, scheduleEnabled: workflow.scheduleEnabled, enabled: workflow.enabled }); setGraphText(JSON.stringify(workflow.graph, null, 2)); }, [workflow]); const onSave = async () => { const values = await form.validateFields(); let graph: WorkflowGraph; try { graph = JSON.parse(graphText); } catch (e: any) { message.error('Graph JSON 解析失败:' + e.message); return; } if (!graph.entry || !Array.isArray(graph.nodes)) { message.error('Graph 必须包含 entry + nodes 数组'); return; } setSaving(true); try { const payload = { ...values, graph }; if (workflow.id) { await WorkflowAPI.update(workflow.id, payload); } else { await WorkflowAPI.create(payload); } message.success('已保存'); onSaved(); } catch (e: any) { message.error('保存失败:' + (e?.message ?? e)); } finally { setSaving(false); } }; return ( 保存 } >
Cron 表达式 5 段:分 时 日 月 周,例如 "*/30 * * * *" } name="scheduleCron" >
{ try { const g: WorkflowGraph = JSON.parse(graphText); g.nodes = [...(g.nodes || []), node]; if (!g.entry) g.entry = node.id; setGraphText(JSON.stringify(g, null, 2)); } catch { message.error('Graph JSON 不合法,无法追加'); } }} /> Graph JSON(节点列表)} style={{ marginTop: 12, borderRadius: 16, boxShadow: 'var(--shadow-xs)' }} extra={ } > setGraphText(e.target.value)} style={{ fontFamily: 'ui-monospace, monospace', fontSize: 12 }} /> 支持模板 {'{{input.x}} {{vars.x}} {{steps..output...}}'}
节点类型:agent / skill / http / transform / branch;branch 节点用 condition(返回 bool 的 JS 片段),引擎按 next/elseNext 走。
); } function NodeQuickAdd({ onAdd }: { onAdd: (n: WorkflowNode) => void }) { const [type, setType] = useState('agent'); const [id, setId] = useState(''); return ( setId(e.target.value)} style={{ width: 200 }} /> ); } function defaultConfig(type: WorkflowNodeType): Record { switch (type) { case 'agent': return { agentId: '', prompt: '请帮我处理:{{input.text}}' }; case 'skill': return { skillId: '', args: {} }; case 'http': return { method: 'GET', url: 'https://example.com/api', headers: {}, body: null }; case 'transform': return { code: 'return { value: input.text };' }; case 'branch': return { condition: 'steps.prev?.output?.value === true' }; } } // ================== 运行抽屉 ================== function RunsDrawer({ open, workflow, onClose }: { open: boolean; workflow: Workflow; onClose: () => void; }) { const { message } = AntApp.useApp(); const [tab, setTab] = useState('run'); const [runs, setRuns] = useState([]); const [detail, setDetail] = useState(null); const [streaming, setStreaming] = useState(false); const [steps, setSteps] = useState([]); const [finalRun, setFinalRun] = useState(null); const [inputJson, setInputJson] = useState('{}'); const loadRuns = async () => { try { const data = await WorkflowAPI.listRuns(workflow.id, 30); setRuns(data); } catch (e: any) { message.error('加载历史失败:' + (e?.message ?? e)); } }; useEffect(() => { if (open) loadRuns(); }, [open]); const onRunStream = () => { let input: any = undefined; if (inputJson.trim()) { try { input = JSON.parse(inputJson); } catch (e: any) { message.error('input JSON 不合法'); return; } } setSteps([]); setFinalRun(null); setStreaming(true); streamWorkflowRun(workflow.id, input, { onStepStart: (d) => { setSteps((s) => [...s, { ...d, status: 'running' }]); }, onStepFinish: (d) => { setSteps((s) => { const idx = [...s].reverse().findIndex((x) => x.nodeId === d.nodeId && x.status === 'running'); if (idx === -1) return [...s, d]; const realIdx = s.length - 1 - idx; const cp = [...s]; cp[realIdx] = d; return cp; }); }, onRunFinish: (d) => { setStreaming(false); setFinalRun(d); loadRuns(); }, onError: (msg) => { setStreaming(false); message.error(msg); } }); }; const showDetail = async (runId: string) => { try { const d = await WorkflowAPI.getRun(runId); setDetail(d); } catch (e: any) { message.error('加载详情失败:' + (e?.message ?? e)); } }; return ( { setDetail(null); onClose(); }} styles={{ body: { background: '#fcfcfd' }, header: { background: '#fff', borderBottom: '1px solid var(--color-border)' } }} >
setInputJson(e.target.value)} placeholder='{"text":"hello"}' />
{steps.length > 0 && ( {steps.map((s, i) => (
{s.status} {s.nodeId} {s.nodeType} {s.durationMs != null && {s.durationMs}ms} {s.error && {s.error}} {s.output != null && (
                            {JSON.stringify(s.output, null, 2)}
                          
)}
))}
)} {finalRun && (
                      {JSON.stringify(finalRun, null, 2)}
                    
)} ) }, { key: 'history', label: `历史 (${runs.length})`, children: (
rowKey="id" size="small" pagination={false} dataSource={runs} columns={[ { title: '状态', dataIndex: 'status', render: (s) => {s}, width: 100 }, { title: '触发', dataIndex: 'trigger', width: 80 }, { title: '开始', dataIndex: 'startedAt', render: (v) => new Date(v).toLocaleString(), width: 180 }, { title: '耗时', dataIndex: 'durationMs', render: (v) => `${v} ms`, width: 100 }, { title: '', render: (_, r) => } ]} /> {detail && ( setDetail(null)} onOk={() => setDetail(null)} width={680} >

状态:{detail.status}{detail.error && {detail.error}}

耗时:{detail.durationMs} ms

Input:

{JSON.stringify(detail.input, null, 2)}

Steps:

{detail.steps.map((s) => ( {s.status} {s.nodeId} {s.nodeType} {s.durationMs} ms {s.error &&

{s.error}

} {s.output != null && (
                            {JSON.stringify(s.output, null, 2)}
                          
)}
))}
)}
) } ]} />
); }