@yuku123/z-agent-frontend-component 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/z-agent-frontend-component.css +1 -0
- package/dist/z-agent-frontend-component.es.js +9956 -0
- package/dist/z-agent-frontend-component.umd.js +219 -0
- package/package.json +77 -0
- package/src/api/apiRouter.js +78 -0
- package/src/api/index.js +23 -0
- package/src/api/request.js +59 -0
- package/src/api/routes.js +140 -0
- package/src/dev.jsx +80 -0
- package/src/index.js +86 -0
- package/src/pages/agent/app/index.jsx +2 -0
- package/src/pages/agent/editor/AgentAppEditor.jsx +456 -0
- package/src/pages/agent/editor/WorkflowEditor.jsx +495 -0
- package/src/pages/agent/editor/nodes/index.ts +225 -0
- package/src/pages/agent/index.jsx +1379 -0
- package/src/pages/agent/share.jsx +512 -0
- package/src/pages/ak/AkUsageDrawer.jsx +208 -0
- package/src/pages/ak/index.jsx +496 -0
- package/src/pages/llm/index.jsx +736 -0
- package/src/pages/llm/model/index.jsx +220 -0
- package/src/pages/llm/provider/index.jsx +173 -0
- package/src/pages/mcp/index.jsx +359 -0
- package/src/pages/oss/BucketList.jsx +320 -0
- package/src/pages/oss/ObjectBrowser.jsx +409 -0
- package/src/pages/product/execute.jsx +608 -0
- package/src/pages/product/index.jsx +628 -0
- package/src/pages/product/scene.jsx +746 -0
- package/src/pages/script/ApiBridgeEditor.jsx +255 -0
- package/src/pages/script/CurlImportModal.jsx +263 -0
- package/src/pages/script/FieldMappingEditor.jsx +131 -0
- package/src/pages/script/OpenApiImportModal.jsx +212 -0
- package/src/pages/script/index.jsx +532 -0
- package/src/pages/skill/index.jsx +1595 -0
- package/src/pages/trace/DebugPlayground.jsx +357 -0
- package/src/pages/trace/components/MetricsDashboard.jsx +164 -0
- package/src/pages/trace/components/RagFragments.jsx +134 -0
- package/src/pages/trace/components/Timeline.jsx +142 -0
- package/src/pages/trace/components/ToolCallTree.jsx +116 -0
- package/src/pages/trace/index.jsx +13 -0
- package/src/pages/usage/index.jsx +352 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {Button, message, Modal, Space, Tag, Tooltip} from 'antd';
|
|
3
|
+
import {DeleteOutlined, RedoOutlined, SaveOutlined, UndoOutlined, CloudUploadOutlined} from '@ant-design/icons';
|
|
4
|
+
import LogicFlow from '@logicflow/core';
|
|
5
|
+
import {Control, DndPanel, Menu, MiniMap, SelectionSelect} from '@logicflow/extension';
|
|
6
|
+
import '@logicflow/core/es/index.css';
|
|
7
|
+
import '@logicflow/extension/es/index.css';
|
|
8
|
+
import {AGENT_FLOW_NODE_TYPES, registerAgentNodes} from './nodes';
|
|
9
|
+
import {flowApi} from '../../../api';
|
|
10
|
+
|
|
11
|
+
// ============ Register LogicFlow extensions ONCE at module level ============
|
|
12
|
+
// LogicFlow.use() is global; calling it inside a component will throw
|
|
13
|
+
// "Extension already registered" on remount.
|
|
14
|
+
LogicFlow.use(Control);
|
|
15
|
+
LogicFlow.use(MiniMap);
|
|
16
|
+
LogicFlow.use(Menu);
|
|
17
|
+
LogicFlow.use(DndPanel);
|
|
18
|
+
LogicFlow.use(SelectionSelect);
|
|
19
|
+
|
|
20
|
+
// ============ Default Starting Flow ============
|
|
21
|
+
const DEFAULT_FLOW_DATA = {
|
|
22
|
+
nodes: [
|
|
23
|
+
{id: 'start_1', type: 'start', x: 150, y: 280, text: '开始'},
|
|
24
|
+
{id: 'llm_1', type: 'llm', x: 380, y: 280, text: 'LLM 调用'},
|
|
25
|
+
{id: 'end_1', type: 'end', x: 610, y: 280, text: '结束'},
|
|
26
|
+
],
|
|
27
|
+
edges: [
|
|
28
|
+
{id: 'e1', type: 'polyline', sourceNodeId: 'start_1', targetNodeId: 'llm_1', text: ''},
|
|
29
|
+
{id: 'e2', type: 'polyline', sourceNodeId: 'llm_1', targetNodeId: 'end_1', text: ''},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ============ Node Config Panel (side panel for selected node) ============
|
|
34
|
+
const NodeConfigPanel = ({node, onChange, onClose}) => {
|
|
35
|
+
if (!node) return null;
|
|
36
|
+
const typeLabel = AGENT_FLOW_NODE_TYPES.find(n => n.type === node.type)?.label || node.type;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div style={{
|
|
40
|
+
position: 'absolute', right: 16, top: 16, width: 260,
|
|
41
|
+
background: '#fff', borderRadius: 8, boxShadow: '0 2px 12px rgba(0,0,0,0.12)',
|
|
42
|
+
padding: 16, zIndex: 10,
|
|
43
|
+
}}>
|
|
44
|
+
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12}}>
|
|
45
|
+
<div>
|
|
46
|
+
<Tag color="blue">{typeLabel}</Tag>
|
|
47
|
+
</div>
|
|
48
|
+
<Button type="text" size="small" onClick={onClose}>✕</Button>
|
|
49
|
+
</div>
|
|
50
|
+
<div style={{marginBottom: 8}}>
|
|
51
|
+
<div style={{fontSize: 12, color: '#666', marginBottom: 4}}>节点 ID</div>
|
|
52
|
+
<div style={{fontSize: 13, color: '#333'}}>{node.id}</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div style={{marginBottom: 8}}>
|
|
55
|
+
<div style={{fontSize: 12, color: '#666', marginBottom: 4}}>显示文本</div>
|
|
56
|
+
<input
|
|
57
|
+
style={{
|
|
58
|
+
width: '100%',
|
|
59
|
+
border: '1px solid #d9d9d9',
|
|
60
|
+
borderRadius: 4,
|
|
61
|
+
padding: '4px 8px',
|
|
62
|
+
fontSize: 13
|
|
63
|
+
}}
|
|
64
|
+
defaultValue={node.text}
|
|
65
|
+
onBlur={(e) => onChange(node.id, {text: e.target.value})}
|
|
66
|
+
placeholder="输入显示文本"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
{node.type === 'llm' && (
|
|
70
|
+
<div style={{marginBottom: 8}}>
|
|
71
|
+
<div style={{fontSize: 12, color: '#666', marginBottom: 4}}>模型</div>
|
|
72
|
+
<select
|
|
73
|
+
style={{
|
|
74
|
+
width: '100%',
|
|
75
|
+
border: '1px solid #d9d9d9',
|
|
76
|
+
borderRadius: 4,
|
|
77
|
+
padding: '4px 8px',
|
|
78
|
+
fontSize: 13
|
|
79
|
+
}}
|
|
80
|
+
defaultValue={node.properties?.model || 'qwen2.5:7b'}
|
|
81
|
+
onChange={(e) => onChange(node.id, {properties: {...node.properties, model: e.target.value}})}
|
|
82
|
+
>
|
|
83
|
+
<option value="qwen2.5:7b">Qwen 2.5 7B</option>
|
|
84
|
+
<option value="qwen2.5:72b">Qwen 2.5 72B</option>
|
|
85
|
+
<option value="gpt-4o">GPT-4o</option>
|
|
86
|
+
</select>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{node.type === 'http' && (
|
|
90
|
+
<>
|
|
91
|
+
<div style={{marginBottom: 8}}>
|
|
92
|
+
<div style={{fontSize: 12, color: '#666', marginBottom: 4}}>HTTP Method</div>
|
|
93
|
+
<select
|
|
94
|
+
style={{
|
|
95
|
+
width: '100%',
|
|
96
|
+
border: '1px solid #d9d9d9',
|
|
97
|
+
borderRadius: 4,
|
|
98
|
+
padding: '4px 8px',
|
|
99
|
+
fontSize: 13
|
|
100
|
+
}}
|
|
101
|
+
defaultValue={node.properties?.method || 'GET'}
|
|
102
|
+
onChange={(e) => onChange(node.id, {
|
|
103
|
+
properties: {
|
|
104
|
+
...node.properties,
|
|
105
|
+
method: e.target.value
|
|
106
|
+
}
|
|
107
|
+
})}
|
|
108
|
+
>
|
|
109
|
+
<option value="GET">GET</option>
|
|
110
|
+
<option value="POST">POST</option>
|
|
111
|
+
<option value="PUT">PUT</option>
|
|
112
|
+
<option value="DELETE">DELETE</option>
|
|
113
|
+
</select>
|
|
114
|
+
</div>
|
|
115
|
+
<div style={{marginBottom: 8}}>
|
|
116
|
+
<div style={{fontSize: 12, color: '#666', marginBottom: 4}}>URL</div>
|
|
117
|
+
<input
|
|
118
|
+
style={{
|
|
119
|
+
width: '100%',
|
|
120
|
+
border: '1px solid #d9d9d9',
|
|
121
|
+
borderRadius: 4,
|
|
122
|
+
padding: '4px 8px',
|
|
123
|
+
fontSize: 13
|
|
124
|
+
}}
|
|
125
|
+
defaultValue={node.properties?.url || ''}
|
|
126
|
+
onBlur={(e) => onChange(node.id, {properties: {...node.properties, url: e.target.value}})}
|
|
127
|
+
placeholder="https://api.example.com/..."
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
{node.type === 'condition' && (
|
|
133
|
+
<div style={{marginBottom: 8}}>
|
|
134
|
+
<div style={{fontSize: 12, color: '#666', marginBottom: 4}}>条件表达式</div>
|
|
135
|
+
<textarea
|
|
136
|
+
style={{
|
|
137
|
+
width: '100%',
|
|
138
|
+
border: '1px solid #d9d9d9',
|
|
139
|
+
borderRadius: 4,
|
|
140
|
+
padding: '4px 8px',
|
|
141
|
+
fontSize: 12,
|
|
142
|
+
resize: 'vertical'
|
|
143
|
+
}}
|
|
144
|
+
rows={3}
|
|
145
|
+
defaultValue={node.properties?.expression || ''}
|
|
146
|
+
onBlur={(e) => onChange(node.id, {
|
|
147
|
+
properties: {
|
|
148
|
+
...node.properties,
|
|
149
|
+
expression: e.target.value
|
|
150
|
+
}
|
|
151
|
+
})}
|
|
152
|
+
placeholder="e.g. status == 'success'"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ============ Main WorkflowEditor Component ============
|
|
161
|
+
const WorkflowEditor = ({readOnly = false, value, onChange, appCode, flowName}) => {
|
|
162
|
+
const containerRef = useRef(null);
|
|
163
|
+
const lfRef = useRef(null);
|
|
164
|
+
const [selectedNode, setSelectedNode] = useState(null);
|
|
165
|
+
const [graphData, setGraphData] = useState(value || DEFAULT_FLOW_DATA);
|
|
166
|
+
// FEATURE013 A5: 后端保存状态
|
|
167
|
+
const [savedFlowId, setSavedFlowId] = useState(null); // 已保存的 flow_definition.id
|
|
168
|
+
const [savingToBackend, setSavingToBackend] = useState(false);
|
|
169
|
+
const [showPublishModal, setShowPublishModal] = useState(false);
|
|
170
|
+
|
|
171
|
+
// Initialize LogicFlow
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!containerRef.current) return;
|
|
174
|
+
|
|
175
|
+
const lf = new LogicFlow({
|
|
176
|
+
container: containerRef.current,
|
|
177
|
+
grid: {
|
|
178
|
+
size: 20,
|
|
179
|
+
visible: true,
|
|
180
|
+
type: 'dot',
|
|
181
|
+
config: {color: '#e5e5e5'},
|
|
182
|
+
},
|
|
183
|
+
keyboard: {enabled: true},
|
|
184
|
+
snapline: true,
|
|
185
|
+
outline: true,
|
|
186
|
+
textEdit: !readOnly,
|
|
187
|
+
isSilentMode: readOnly,
|
|
188
|
+
style: {
|
|
189
|
+
rect: {rx: 6, ry: 6},
|
|
190
|
+
polyline: {stroke: '#bfbfbf', strokeWidth: 2},
|
|
191
|
+
},
|
|
192
|
+
// Default edge type
|
|
193
|
+
edgeType: 'polyline',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Register custom Agent nodes
|
|
197
|
+
registerAgentNodes(lf);
|
|
198
|
+
|
|
199
|
+
// Set up drag-and-drop palette
|
|
200
|
+
lf.extension.dndPanel.setPatternItems(
|
|
201
|
+
AGENT_FLOW_NODE_TYPES.map(n => ({
|
|
202
|
+
type: n.type,
|
|
203
|
+
label: n.label,
|
|
204
|
+
icon: n.icon,
|
|
205
|
+
}))
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Show mini map via extension API (NOT as a React component)
|
|
209
|
+
if (lf.extension.miniMap) {
|
|
210
|
+
lf.extension.miniMap.show({
|
|
211
|
+
width: 160,
|
|
212
|
+
height: 100,
|
|
213
|
+
showEdge: true,
|
|
214
|
+
isShowHeader: false,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Load initial data
|
|
219
|
+
const initialData = value || DEFAULT_FLOW_DATA;
|
|
220
|
+
lf.render(initialData);
|
|
221
|
+
setGraphData(initialData);
|
|
222
|
+
|
|
223
|
+
// Node click → show config panel
|
|
224
|
+
lf.on('node:click', ({data}) => {
|
|
225
|
+
if (readOnly) return;
|
|
226
|
+
setSelectedNode(data);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Blank click → deselect
|
|
230
|
+
lf.on('blank:click', () => {
|
|
231
|
+
setSelectedNode(null);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Graph change → notify parent
|
|
235
|
+
lf.on('graph:change', () => {
|
|
236
|
+
const data = lf.getGraphData();
|
|
237
|
+
setGraphData(data);
|
|
238
|
+
onChange?.(data);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// History
|
|
242
|
+
lfRef.current = lf;
|
|
243
|
+
|
|
244
|
+
return () => {
|
|
245
|
+
lf.destroy();
|
|
246
|
+
};
|
|
247
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
248
|
+
|
|
249
|
+
// Update config panel when node changes
|
|
250
|
+
const handleNodeConfigChange = useCallback((nodeId, changes) => {
|
|
251
|
+
if (!lfRef.current) return;
|
|
252
|
+
lfRef.current.updateText(nodeId, changes.text || '');
|
|
253
|
+
if (changes.properties) {
|
|
254
|
+
lfRef.current.updateProperties(nodeId, changes.properties);
|
|
255
|
+
}
|
|
256
|
+
// Refresh selected node data
|
|
257
|
+
const node = lfRef.current.getGraphData().nodes.find(n => n.id === nodeId);
|
|
258
|
+
if (node) setSelectedNode(node);
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
// Toolbar actions
|
|
262
|
+
const handleAddNode = useCallback((type) => {
|
|
263
|
+
if (!lfRef.current || readOnly) return;
|
|
264
|
+
const center = lfRef.current.getGraphData();
|
|
265
|
+
const avgX = center.nodes.reduce((s, n) => s + n.x, 0) / (center.nodes.length || 1);
|
|
266
|
+
const avgY = center.nodes.reduce((s, n) => s + n.y, 0) / (center.nodes.length || 1);
|
|
267
|
+
lfRef.current.addNode({
|
|
268
|
+
type,
|
|
269
|
+
x: avgX + 80,
|
|
270
|
+
y: avgY,
|
|
271
|
+
text: AGENT_FLOW_NODE_TYPES.find(n => n.type === type)?.label || type
|
|
272
|
+
});
|
|
273
|
+
}, [readOnly]);
|
|
274
|
+
|
|
275
|
+
const handleDeleteSelected = useCallback(() => {
|
|
276
|
+
if (!lfRef.current || readOnly) return;
|
|
277
|
+
const selected = lfRef.current.getSelectedModels();
|
|
278
|
+
if (selected.nodes?.length) {
|
|
279
|
+
selected.nodes.forEach(n => lfRef.current.deleteNode(n.id));
|
|
280
|
+
}
|
|
281
|
+
if (selected.edges?.length) {
|
|
282
|
+
selected.edges.forEach(e => lfRef.current.deleteEdge(e.id));
|
|
283
|
+
}
|
|
284
|
+
setSelectedNode(null);
|
|
285
|
+
}, [readOnly]);
|
|
286
|
+
|
|
287
|
+
const handleUndo = useCallback(() => {
|
|
288
|
+
if (!lfRef.current || readOnly) return;
|
|
289
|
+
lfRef.current.undo();
|
|
290
|
+
}, [readOnly]);
|
|
291
|
+
|
|
292
|
+
const handleRedo = useCallback(() => {
|
|
293
|
+
if (!lfRef.current || readOnly) return;
|
|
294
|
+
lfRef.current.redo();
|
|
295
|
+
}, [readOnly]);
|
|
296
|
+
|
|
297
|
+
const handleSave = useCallback(() => {
|
|
298
|
+
if (!lfRef.current) return;
|
|
299
|
+
const data = lfRef.current.getGraphData();
|
|
300
|
+
setGraphData(data);
|
|
301
|
+
onChange?.(data);
|
|
302
|
+
message.success('流程已保存到前端');
|
|
303
|
+
}, [onChange]);
|
|
304
|
+
|
|
305
|
+
// FEATURE013 A5: 保存到后端 (创建或更新 z_agent_flow_definition)
|
|
306
|
+
const handleSaveToBackend = useCallback(async () => {
|
|
307
|
+
if (!lfRef.current || !appCode) {
|
|
308
|
+
message.warning('缺少 appCode, 无法保存到后端');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
setSavingToBackend(true);
|
|
312
|
+
try {
|
|
313
|
+
const data = lfRef.current.getGraphData();
|
|
314
|
+
const payload = {
|
|
315
|
+
flowId: appCode,
|
|
316
|
+
name: flowName || appCode,
|
|
317
|
+
description: `App ${appCode} 的工作流`,
|
|
318
|
+
nodes: JSON.stringify(data.nodes || []),
|
|
319
|
+
edges: JSON.stringify(data.edges || []),
|
|
320
|
+
variables: JSON.stringify(data.variables || {}),
|
|
321
|
+
flowType: 'AGENT',
|
|
322
|
+
appCode: appCode,
|
|
323
|
+
status: 'DRAFT',
|
|
324
|
+
version: 1,
|
|
325
|
+
creator: localStorage.getItem('userInfo')
|
|
326
|
+
? JSON.parse(localStorage.getItem('userInfo') || '{}').userCode || 'anonymous'
|
|
327
|
+
: 'anonymous',
|
|
328
|
+
};
|
|
329
|
+
let resp;
|
|
330
|
+
if (savedFlowId) {
|
|
331
|
+
// 已存在, 走 update
|
|
332
|
+
resp = await flowApi.update({...payload, id: savedFlowId});
|
|
333
|
+
} else {
|
|
334
|
+
// 首次, 走 create
|
|
335
|
+
resp = await flowApi.create(payload);
|
|
336
|
+
const result = resp?.data ?? resp;
|
|
337
|
+
if (result && typeof result === 'number') {
|
|
338
|
+
setSavedFlowId(result);
|
|
339
|
+
} else if (result && result.id) {
|
|
340
|
+
setSavedFlowId(result.id);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
message.success('流程已保存到后端 z_agent_flow_definition');
|
|
344
|
+
// 同步给父组件
|
|
345
|
+
onChange?.(data);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
message.error('保存到后端失败: ' + (e?.message || '未知错误'));
|
|
348
|
+
} finally {
|
|
349
|
+
setSavingToBackend(false);
|
|
350
|
+
}
|
|
351
|
+
}, [appCode, flowName, savedFlowId, onChange]);
|
|
352
|
+
|
|
353
|
+
// FEATURE013 A5: 发布 (DRAFT → PUBLISHED, 写入执行内存)
|
|
354
|
+
const handlePublish = useCallback(async () => {
|
|
355
|
+
if (!savedFlowId) {
|
|
356
|
+
message.warning('请先点击"保存到后端"');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await flowApi.publish(savedFlowId);
|
|
361
|
+
setShowPublishModal(false);
|
|
362
|
+
message.success('流程已发布, 现在可以被 Agent Runtime 执行');
|
|
363
|
+
} catch (e) {
|
|
364
|
+
message.error('发布失败: ' + (e?.message || '未知错误'));
|
|
365
|
+
}
|
|
366
|
+
}, [savedFlowId]);
|
|
367
|
+
|
|
368
|
+
// FEATURE013 A5: 启动时尝试加载已有的 flow (按 appCode 找)
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (!appCode || readOnly) return;
|
|
371
|
+
(async () => {
|
|
372
|
+
try {
|
|
373
|
+
const resp = await flowApi.byFlowId({flowId: appCode, version: 1});
|
|
374
|
+
const result = resp?.data ?? resp;
|
|
375
|
+
if (result && result.id && result.nodes) {
|
|
376
|
+
setSavedFlowId(result.id);
|
|
377
|
+
// 把后端的 nodes/edges 灌入 LogicFlow
|
|
378
|
+
if (lfRef.current) {
|
|
379
|
+
lfRef.current.render({
|
|
380
|
+
nodes: JSON.parse(result.nodes),
|
|
381
|
+
edges: JSON.parse(result.edges),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
message.info('已加载上次保存的工作流 v' + (result.version || 1));
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
// 首次保存, 不报错
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
390
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
391
|
+
}, [appCode]);
|
|
392
|
+
|
|
393
|
+
const handleCloseConfig = useCallback(() => {
|
|
394
|
+
setSelectedNode(null);
|
|
395
|
+
}, []);
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
<div style={{
|
|
399
|
+
position: 'relative',
|
|
400
|
+
height: 520,
|
|
401
|
+
borderRadius: 8,
|
|
402
|
+
overflow: 'hidden',
|
|
403
|
+
background: '#fafafa',
|
|
404
|
+
border: '1px solid #e8e8e8'
|
|
405
|
+
}}>
|
|
406
|
+
{/* Toolbar */}
|
|
407
|
+
{!readOnly && (
|
|
408
|
+
<div style={{
|
|
409
|
+
position: 'absolute', top: 8, left: 8, zIndex: 5,
|
|
410
|
+
display: 'flex', gap: 6, alignItems: 'center',
|
|
411
|
+
background: '#fff', borderRadius: 6, padding: '4px 8px',
|
|
412
|
+
boxShadow: '0 1px 6px rgba(0,0,0,0.08)',
|
|
413
|
+
}}>
|
|
414
|
+
<Space size={4}>
|
|
415
|
+
<Tooltip title="撤销"><Button size="small" icon={<UndoOutlined/>}
|
|
416
|
+
onClick={handleUndo}/></Tooltip>
|
|
417
|
+
<Tooltip title="重做"><Button size="small" icon={<RedoOutlined/>}
|
|
418
|
+
onClick={handleRedo}/></Tooltip>
|
|
419
|
+
<div style={{width: 1, height: 16, background: '#e8e8e8', margin: '0 2px'}}/>
|
|
420
|
+
<Tooltip title="删除选中"><Button size="small" danger icon={<DeleteOutlined/>}
|
|
421
|
+
onClick={handleDeleteSelected}/></Tooltip>
|
|
422
|
+
<Tooltip title="保存到前端 (AgentApp)">
|
|
423
|
+
<Button size="small" icon={<SaveOutlined/>} onClick={handleSave}/>
|
|
424
|
+
</Tooltip>
|
|
425
|
+
{/* FEATURE013 A5: 后端保存 + 发布 */}
|
|
426
|
+
<div style={{width: 1, height: 16, background: '#e8e8e8', margin: '0 2px'}}/>
|
|
427
|
+
<Tooltip title="保存到后端 z_agent_flow_definition (FEATURE013 A5)">
|
|
428
|
+
<Button size="small" type="primary" ghost
|
|
429
|
+
icon={<CloudUploadOutlined/>}
|
|
430
|
+
loading={savingToBackend}
|
|
431
|
+
disabled={!appCode}
|
|
432
|
+
onClick={handleSaveToBackend}>
|
|
433
|
+
保存流程
|
|
434
|
+
</Button>
|
|
435
|
+
</Tooltip>
|
|
436
|
+
<Tooltip title="发布 (DRAFT → PUBLISHED, FlowEngine 可加载执行)">
|
|
437
|
+
<Button size="small" type="primary"
|
|
438
|
+
disabled={!savedFlowId}
|
|
439
|
+
onClick={() => setShowPublishModal(true)}>
|
|
440
|
+
发布
|
|
441
|
+
</Button>
|
|
442
|
+
</Tooltip>
|
|
443
|
+
</Space>
|
|
444
|
+
{savedFlowId && (
|
|
445
|
+
<Tag color="blue" style={{marginLeft: 8}}>flowId={savedFlowId}</Tag>
|
|
446
|
+
)}
|
|
447
|
+
<div style={{fontSize: 12, color: '#999', marginLeft: 8}}>
|
|
448
|
+
从左侧拖入节点到画布,双击节点编辑文本
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
)}
|
|
452
|
+
|
|
453
|
+
{/* FEATURE013 A5: 发布确认 Modal */}
|
|
454
|
+
<Modal
|
|
455
|
+
title="发布工作流"
|
|
456
|
+
open={showPublishModal}
|
|
457
|
+
onOk={handlePublish}
|
|
458
|
+
onCancel={() => setShowPublishModal(false)}
|
|
459
|
+
okText="确认发布"
|
|
460
|
+
cancelText="取消"
|
|
461
|
+
>
|
|
462
|
+
<p>发布后, <b>FlowOrchestrationService</b> 会在下次请求时加载这个 flow,
|
|
463
|
+
Agent Runtime 可以通过 <code>/api/agent/flow/execute/{savedFlowId}</code> 触发执行。</p>
|
|
464
|
+
<p style={{color: '#999', fontSize: 12}}>提示: 如需修改, 需点击「升级到新版本」创建草稿。</p>
|
|
465
|
+
</Modal>
|
|
466
|
+
|
|
467
|
+
{/* LogicFlow Canvas */}
|
|
468
|
+
<div ref={containerRef} style={{width: '100%', height: '100%'}}/>
|
|
469
|
+
|
|
470
|
+
{/* Node Config Side Panel */}
|
|
471
|
+
{selectedNode && !readOnly && (
|
|
472
|
+
<NodeConfigPanel
|
|
473
|
+
node={selectedNode}
|
|
474
|
+
onChange={handleNodeConfigChange}
|
|
475
|
+
onClose={handleCloseConfig}
|
|
476
|
+
/>
|
|
477
|
+
)}
|
|
478
|
+
|
|
479
|
+
{/* MiniMap 已通过 lf.extension.miniMap.show() 渲染到 LogicFlow 容器内,不需要额外 React 元素 */}
|
|
480
|
+
|
|
481
|
+
{/* Read-only badge */}
|
|
482
|
+
{readOnly && (
|
|
483
|
+
<div style={{
|
|
484
|
+
position: 'absolute', top: 8, right: 8, zIndex: 5,
|
|
485
|
+
background: 'rgba(0,0,0,0.5)', color: '#fff', borderRadius: 4,
|
|
486
|
+
padding: '2px 8px', fontSize: 12,
|
|
487
|
+
}}>
|
|
488
|
+
只读模式
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
export default WorkflowEditor;
|