@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,255 @@
|
|
|
1
|
+
import {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import {Button, Card, Input, Select, Space, Switch, Table, Tag, Tooltip, Typography} from 'antd'
|
|
3
|
+
import {DeleteOutlined, PlusOutlined} from '@ant-design/icons'
|
|
4
|
+
import FieldMappingEditor from './FieldMappingEditor'
|
|
5
|
+
|
|
6
|
+
const {Text, Paragraph} = Typography
|
|
7
|
+
|
|
8
|
+
const METHOD_OPTIONS = [
|
|
9
|
+
{value: 'GET', label: 'GET'},
|
|
10
|
+
{value: 'POST', label: 'POST'},
|
|
11
|
+
{value: 'PUT', label: 'PUT'},
|
|
12
|
+
{value: 'DELETE', label: 'DELETE'},
|
|
13
|
+
{value: 'PATCH', label: 'PATCH'},
|
|
14
|
+
{value: 'HEAD', label: 'HEAD'},
|
|
15
|
+
{value: 'OPTIONS', label: 'OPTIONS'},
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const IN_OPTIONS = [
|
|
19
|
+
{value: 'path', label: 'path'},
|
|
20
|
+
{value: 'query', label: 'query'},
|
|
21
|
+
{value: 'header', label: 'header'},
|
|
22
|
+
{value: 'body', label: 'body'},
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* API Bridge 结构化编辑器
|
|
27
|
+
*
|
|
28
|
+
* value: string (sourceCode JSON — ApiBridgeDefinition)
|
|
29
|
+
* onChange: (newSourceCode: string) => void
|
|
30
|
+
*/
|
|
31
|
+
export default function ApiBridgeEditor({value, onChange, readonly = false}) {
|
|
32
|
+
const [def, setDef] = useState(null)
|
|
33
|
+
const [parseError, setParseError] = useState(null)
|
|
34
|
+
|
|
35
|
+
// Parse sourceCode JSON on mount / value change
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!value) {
|
|
38
|
+
setDef({
|
|
39
|
+
request: {
|
|
40
|
+
httpRequestLine: {url: '', requestMethod: 'GET'},
|
|
41
|
+
httpRequestHeader: {headers: {}},
|
|
42
|
+
httpRequestBody: {body: null},
|
|
43
|
+
},
|
|
44
|
+
inputParams: [],
|
|
45
|
+
outputMapping: [],
|
|
46
|
+
})
|
|
47
|
+
setParseError(null)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
|
52
|
+
// Normalize structure
|
|
53
|
+
if (!parsed.request) parsed.request = {}
|
|
54
|
+
if (!parsed.request.httpRequestLine) parsed.request.httpRequestLine = {}
|
|
55
|
+
if (!parsed.request.httpRequestHeader) parsed.request.httpRequestHeader = {}
|
|
56
|
+
if (!parsed.request.httpRequestBody) parsed.request.httpRequestBody = {}
|
|
57
|
+
if (!parsed.inputParams) parsed.inputParams = []
|
|
58
|
+
if (!parsed.outputMapping) parsed.outputMapping = []
|
|
59
|
+
setDef(parsed)
|
|
60
|
+
setParseError(null)
|
|
61
|
+
} catch (e) {
|
|
62
|
+
setParseError('JSON 解析失败: ' + e.message)
|
|
63
|
+
}
|
|
64
|
+
}, [value])
|
|
65
|
+
|
|
66
|
+
// Emit change
|
|
67
|
+
const emit = useCallback((next) => {
|
|
68
|
+
setDef(next)
|
|
69
|
+
onChange?.(JSON.stringify(next, null, 2))
|
|
70
|
+
}, [onChange])
|
|
71
|
+
|
|
72
|
+
if (parseError) {
|
|
73
|
+
return (
|
|
74
|
+
<Card size="small" style={{borderColor: '#ff4d4f'}}>
|
|
75
|
+
<Text type="danger">{parseError}</Text>
|
|
76
|
+
<pre style={{fontFamily: 'monospace', fontSize: 12, marginTop: 8, maxHeight: 200, overflow: 'auto'}}>
|
|
77
|
+
{typeof value === 'string' ? value.substring(0, 500) : String(value)}
|
|
78
|
+
</pre>
|
|
79
|
+
</Card>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!def) return null
|
|
84
|
+
|
|
85
|
+
const line = def.request?.httpRequestLine || {}
|
|
86
|
+
const headers = def.request?.httpRequestHeader?.headers || {}
|
|
87
|
+
|
|
88
|
+
// --- Request info update ---
|
|
89
|
+
const updateMethod = (m) => {
|
|
90
|
+
const next = JSON.parse(JSON.stringify(def))
|
|
91
|
+
next.request.httpRequestLine.requestMethod = m
|
|
92
|
+
emit(next)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const updateUrl = (u) => {
|
|
96
|
+
const next = JSON.parse(JSON.stringify(def))
|
|
97
|
+
next.request.httpRequestLine.url = u
|
|
98
|
+
emit(next)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Input params ---
|
|
102
|
+
const updateParam = (index, field, val) => {
|
|
103
|
+
const next = JSON.parse(JSON.stringify(def))
|
|
104
|
+
next.inputParams[index][field] = val
|
|
105
|
+
emit(next)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const removeParam = (index) => {
|
|
109
|
+
const next = JSON.parse(JSON.stringify(def))
|
|
110
|
+
next.inputParams = next.inputParams.filter((_, i) => i !== index)
|
|
111
|
+
emit(next)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const addParam = () => {
|
|
115
|
+
const next = JSON.parse(JSON.stringify(def))
|
|
116
|
+
next.inputParams.push({name: '', in: 'query', required: false, description: '', defaultValue: ''})
|
|
117
|
+
emit(next)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Output mapping ---
|
|
121
|
+
const updateOutputMapping = (list) => {
|
|
122
|
+
const next = JSON.parse(JSON.stringify(def))
|
|
123
|
+
next.outputMapping = list
|
|
124
|
+
emit(next)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const paramColumns = [
|
|
128
|
+
{
|
|
129
|
+
title: '参数名',
|
|
130
|
+
dataIndex: 'name',
|
|
131
|
+
width: 140,
|
|
132
|
+
render: (v, _, i) => readonly ? <Text code>{v}</Text> : (
|
|
133
|
+
<Input size="small" value={v} onChange={e => updateParam(i, 'name', e.target.value)}
|
|
134
|
+
placeholder="param name"/>
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
title: '位置',
|
|
139
|
+
dataIndex: 'in',
|
|
140
|
+
width: 100,
|
|
141
|
+
render: (v, _, i) => readonly ? <Tag>{v}</Tag> : (
|
|
142
|
+
<Select size="small" value={v} onChange={v2 => updateParam(i, 'in', v2)} options={IN_OPTIONS}
|
|
143
|
+
style={{width: '100%'}}/>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
title: '必填',
|
|
148
|
+
dataIndex: 'required',
|
|
149
|
+
width: 70,
|
|
150
|
+
render: (v, _, i) => readonly ? (
|
|
151
|
+
<span style={{color: v ? '#ff4d4f' : '#999'}}>{v ? '是' : '否'}</span>
|
|
152
|
+
) : (
|
|
153
|
+
<Switch size="small" checked={v} onChange={v2 => updateParam(i, 'required', v2)}/>
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: '默认值',
|
|
158
|
+
dataIndex: 'defaultValue',
|
|
159
|
+
width: 120,
|
|
160
|
+
render: (v, _, i) => readonly ? <span>{v || '-'}</span> : (
|
|
161
|
+
<Input size="small" value={v || ''} onChange={e => updateParam(i, 'defaultValue', e.target.value)}
|
|
162
|
+
placeholder="default"/>
|
|
163
|
+
),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
title: '描述',
|
|
167
|
+
dataIndex: 'description',
|
|
168
|
+
render: (v, _, i) => readonly ? <span style={{color: '#666'}}>{v}</span> : (
|
|
169
|
+
<Input size="small" value={v} onChange={e => updateParam(i, 'description', e.target.value)}
|
|
170
|
+
placeholder="描述"/>
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
...(!readonly ? [{
|
|
174
|
+
title: '',
|
|
175
|
+
key: 'action',
|
|
176
|
+
width: 50,
|
|
177
|
+
render: (_, __, i) => (
|
|
178
|
+
<Tooltip title="删除">
|
|
179
|
+
<Button size="small" type="text" danger icon={<DeleteOutlined/>} onClick={() => removeParam(i)}/>
|
|
180
|
+
</Tooltip>
|
|
181
|
+
),
|
|
182
|
+
}] : []),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div>
|
|
187
|
+
{/* Request Info */}
|
|
188
|
+
<Card size="small" title="请求配置" style={{marginBottom: 16}}>
|
|
189
|
+
<Space style={{width: '100%'}} direction="vertical" size="small">
|
|
190
|
+
<Space align="center" style={{width: '100%'}}>
|
|
191
|
+
{readonly ? (
|
|
192
|
+
<Tag color="blue"
|
|
193
|
+
style={{fontSize: 14, padding: '2px 12px'}}>{line.requestMethod || 'GET'}</Tag>
|
|
194
|
+
) : (
|
|
195
|
+
<Select
|
|
196
|
+
value={line.requestMethod || 'GET'}
|
|
197
|
+
onChange={updateMethod}
|
|
198
|
+
options={METHOD_OPTIONS}
|
|
199
|
+
style={{width: 120}}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
{readonly ? (
|
|
203
|
+
<Text code style={{fontSize: 13, wordBreak: 'break-all'}}>{line.url}</Text>
|
|
204
|
+
) : (
|
|
205
|
+
<Input
|
|
206
|
+
value={line.url || ''}
|
|
207
|
+
onChange={e => updateUrl(e.target.value)}
|
|
208
|
+
placeholder="https://api.example.com/resource/{id}?key=${key}"
|
|
209
|
+
style={{fontFamily: 'monospace', flex: 1}}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</Space>
|
|
213
|
+
{Object.keys(headers).length > 0 && (
|
|
214
|
+
<div>
|
|
215
|
+
<Text type="secondary" style={{fontSize: 12}}>请求头: </Text>
|
|
216
|
+
{Object.entries(headers).map(([k, v]) => (
|
|
217
|
+
<Tag key={k} style={{fontFamily: 'monospace', fontSize: 11}}>
|
|
218
|
+
{k}: {String(v).substring(0, 50)}
|
|
219
|
+
</Tag>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</Space>
|
|
224
|
+
</Card>
|
|
225
|
+
|
|
226
|
+
{/* Input Params */}
|
|
227
|
+
<Card
|
|
228
|
+
size="small"
|
|
229
|
+
title={`输入参数 (${def.inputParams?.length || 0})`}
|
|
230
|
+
style={{marginBottom: 16}}
|
|
231
|
+
extra={!readonly && (
|
|
232
|
+
<Button size="small" type="link" icon={<PlusOutlined/>} onClick={addParam}>添加</Button>
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
<Table
|
|
236
|
+
dataSource={def.inputParams || []}
|
|
237
|
+
columns={paramColumns}
|
|
238
|
+
rowKey={(_, i) => i}
|
|
239
|
+
pagination={false}
|
|
240
|
+
size="small"
|
|
241
|
+
locale={{emptyText: '暂无输入参数'}}
|
|
242
|
+
/>
|
|
243
|
+
</Card>
|
|
244
|
+
|
|
245
|
+
{/* Output Mapping */}
|
|
246
|
+
<Card size="small" title={`输出字段映射 (${def.outputMapping?.length || 0})`}>
|
|
247
|
+
<FieldMappingEditor
|
|
248
|
+
value={def.outputMapping || []}
|
|
249
|
+
onChange={updateOutputMapping}
|
|
250
|
+
readonly={readonly}
|
|
251
|
+
/>
|
|
252
|
+
</Card>
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import {useState} from 'react'
|
|
2
|
+
import {Alert, Button, Card, Descriptions, Input, message, Modal, Space, Steps, Tabs, Tag} from 'antd'
|
|
3
|
+
import {ApiOutlined, CodeOutlined, ExperimentOutlined} from '@ant-design/icons'
|
|
4
|
+
import {scriptApi} from '../../api'
|
|
5
|
+
import FieldMappingEditor from './FieldMappingEditor'
|
|
6
|
+
|
|
7
|
+
const {TextArea} = Input
|
|
8
|
+
const {Step} = Steps
|
|
9
|
+
|
|
10
|
+
export default function CurlImportModal({open, onClose, onSuccess}) {
|
|
11
|
+
const [current, setCurrent] = useState(0)
|
|
12
|
+
const [loading, setLoading] = useState(false)
|
|
13
|
+
const [previewLoading, setPreviewLoading] = useState(false)
|
|
14
|
+
const [curl, setCurl] = useState('')
|
|
15
|
+
const [parsed, setParsed] = useState(null) // 解析结果
|
|
16
|
+
const [outputMapping, setOutputMapping] = useState([])
|
|
17
|
+
const [previewResult, setPreviewResult] = useState(null)
|
|
18
|
+
const [activeTab, setActiveTab] = useState('mapping')
|
|
19
|
+
|
|
20
|
+
// 重置状态
|
|
21
|
+
const reset = () => {
|
|
22
|
+
setCurrent(0)
|
|
23
|
+
setCurl('')
|
|
24
|
+
setParsed(null)
|
|
25
|
+
setOutputMapping([])
|
|
26
|
+
setPreviewResult(null)
|
|
27
|
+
setActiveTab('mapping')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const handleClose = () => {
|
|
31
|
+
reset()
|
|
32
|
+
onClose()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Step 1: 解析 curl
|
|
36
|
+
const handleParse = async () => {
|
|
37
|
+
if (!curl.trim()) {
|
|
38
|
+
message.warning('请粘贴 curl 命令')
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
setLoading(true)
|
|
42
|
+
try {
|
|
43
|
+
const res = await scriptApi.importCurl({curl})
|
|
44
|
+
if (res?.success) {
|
|
45
|
+
setParsed(res)
|
|
46
|
+
// 默认从 outputMapping 恢复,如果没有则为空
|
|
47
|
+
setOutputMapping(res.outputMapping || [])
|
|
48
|
+
setCurrent(1)
|
|
49
|
+
} else {
|
|
50
|
+
message.error(res?.message || '解析失败')
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
message.error('解析失败: ' + (e?.message || '网络错误'))
|
|
54
|
+
} finally {
|
|
55
|
+
setLoading(false)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Step 2: 预览执行
|
|
60
|
+
const handlePreview = async () => {
|
|
61
|
+
setPreviewLoading(true)
|
|
62
|
+
try {
|
|
63
|
+
const res = await scriptApi.previewMapping({
|
|
64
|
+
curl,
|
|
65
|
+
outputMapping: outputMapping.filter(m => m.name && m.jsonPath),
|
|
66
|
+
})
|
|
67
|
+
setPreviewResult(res)
|
|
68
|
+
setActiveTab('result')
|
|
69
|
+
} catch (e) {
|
|
70
|
+
message.error('预览失败: ' + (e?.message || '网络错误'))
|
|
71
|
+
} finally {
|
|
72
|
+
setPreviewLoading(false)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 3: 确认创建
|
|
77
|
+
const handleCreate = async () => {
|
|
78
|
+
setLoading(true)
|
|
79
|
+
try {
|
|
80
|
+
const res = await scriptApi.importCurl({
|
|
81
|
+
curl,
|
|
82
|
+
outputMapping: outputMapping.filter(m => m.name && m.jsonPath),
|
|
83
|
+
})
|
|
84
|
+
if (res?.success) {
|
|
85
|
+
message.success(`脚本 [${res.scriptCode}] 创建成功`)
|
|
86
|
+
reset()
|
|
87
|
+
onSuccess?.()
|
|
88
|
+
onClose()
|
|
89
|
+
} else {
|
|
90
|
+
message.error(res?.message || '创建失败')
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
message.error('创建失败: ' + (e?.message || '网络错误'))
|
|
94
|
+
} finally {
|
|
95
|
+
setLoading(false)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const requestDescr = parsed?.request?.httpRequestLine
|
|
100
|
+
const headers = parsed?.request?.httpRequestHeader?.headers
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Modal
|
|
104
|
+
title={<Space><ApiOutlined/>从 Curl 导入 API Bridge 脚本</Space>}
|
|
105
|
+
open={open}
|
|
106
|
+
onCancel={handleClose}
|
|
107
|
+
width={900}
|
|
108
|
+
footer={null}
|
|
109
|
+
destroyOnClose
|
|
110
|
+
>
|
|
111
|
+
<Steps current={current} size="small" style={{marginBottom: 24}}>
|
|
112
|
+
<Step title="粘贴 curl" description="输入 curl 命令"/>
|
|
113
|
+
<Step title="配置映射" description="设置字段映射"/>
|
|
114
|
+
<Step title="确认创建" description="保存脚本"/>
|
|
115
|
+
</Steps>
|
|
116
|
+
|
|
117
|
+
{/* Step 0: 粘贴 curl */}
|
|
118
|
+
{current === 0 && (
|
|
119
|
+
<div>
|
|
120
|
+
<p style={{color: '#666', marginBottom: 8}}>
|
|
121
|
+
粘贴任意 HTTP curl 命令,系统将自动解析并生成 API_BRIDGE 脚本
|
|
122
|
+
</p>
|
|
123
|
+
<TextArea
|
|
124
|
+
rows={8}
|
|
125
|
+
value={curl}
|
|
126
|
+
onChange={e => setCurl(e.target.value)}
|
|
127
|
+
placeholder={`curl -X POST https://api.example.com/users \\
|
|
128
|
+
-H "Authorization: Bearer \${token}" \\
|
|
129
|
+
-H "Content-Type: application/json" \\
|
|
130
|
+
-d '{"name": "\${name}", "email": "\${email}"}'`}
|
|
131
|
+
style={{fontFamily: 'monospace', fontSize: 13, marginBottom: 12}}
|
|
132
|
+
/>
|
|
133
|
+
<Button type="primary" icon={<CodeOutlined/>} onClick={handleParse} loading={loading} block>
|
|
134
|
+
解析 curl
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Step 1: 配置映射 */}
|
|
140
|
+
{current === 1 && parsed && (
|
|
141
|
+
<div>
|
|
142
|
+
{/* 请求概览 */}
|
|
143
|
+
<Card size="small" title="请求信息" style={{marginBottom: 16}}>
|
|
144
|
+
<Descriptions column={3} size="small">
|
|
145
|
+
<Descriptions.Item label="方法"><Tag
|
|
146
|
+
color="blue">{requestDescr?.method || 'GET'}</Tag></Descriptions.Item>
|
|
147
|
+
<Descriptions.Item label="URL" span={2}>
|
|
148
|
+
<Text copyable
|
|
149
|
+
style={{fontFamily: 'monospace', fontSize: 12}}>{requestDescr?.url}</Text>
|
|
150
|
+
</Descriptions.Item>
|
|
151
|
+
</Descriptions>
|
|
152
|
+
{headers && Object.keys(headers).length > 0 && (
|
|
153
|
+
<div style={{marginTop: 8}}>
|
|
154
|
+
<span style={{fontSize: 12, color: '#666', fontWeight: 500}}>请求头: </span>
|
|
155
|
+
{Object.entries(headers).map(([k, v]) => (
|
|
156
|
+
<Tag key={k} style={{
|
|
157
|
+
fontFamily: 'monospace',
|
|
158
|
+
fontSize: 11
|
|
159
|
+
}}>{k}: {String(v).substring(0, 40)}</Tag>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
{parsed.inputParams?.length > 0 && (
|
|
164
|
+
<div style={{marginTop: 8}}>
|
|
165
|
+
<span style={{fontSize: 12, color: '#666', fontWeight: 500}}>输入参数: </span>
|
|
166
|
+
{parsed.inputParams.map(p => (
|
|
167
|
+
<Tag key={p.name} color={p.required ? 'orange' : 'default'}
|
|
168
|
+
style={{fontFamily: 'monospace'}}>
|
|
169
|
+
{p.name}{' '}
|
|
170
|
+
<span style={{fontSize: 10}}>({p.in})</span>
|
|
171
|
+
</Tag>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</Card>
|
|
176
|
+
|
|
177
|
+
{/* 字段映射 + 预览 */}
|
|
178
|
+
<Tabs activeKey={activeTab} onChange={setActiveTab} items={[
|
|
179
|
+
{
|
|
180
|
+
key: 'mapping',
|
|
181
|
+
label: '输出字段映射',
|
|
182
|
+
children: (
|
|
183
|
+
<div>
|
|
184
|
+
<p style={{fontSize: 12, color: '#666', marginBottom: 8}}>
|
|
185
|
+
配置 JSONPath 从响应中抽取字段。MCP 工具只返回映射后的字段。
|
|
186
|
+
</p>
|
|
187
|
+
<FieldMappingEditor value={outputMapping} onChange={setOutputMapping}/>
|
|
188
|
+
</div>
|
|
189
|
+
),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'result',
|
|
193
|
+
label: '试运行结果',
|
|
194
|
+
children: (
|
|
195
|
+
<div>
|
|
196
|
+
<Button icon={<ExperimentOutlined/>} onClick={handlePreview}
|
|
197
|
+
loading={previewLoading} style={{marginBottom: 12}}>
|
|
198
|
+
试运行并查看映射结果
|
|
199
|
+
</Button>
|
|
200
|
+
{previewResult && (
|
|
201
|
+
<div>
|
|
202
|
+
{previewResult.error ? (
|
|
203
|
+
<Alert type="error" message={previewResult.error}
|
|
204
|
+
style={{marginBottom: 12}}/>
|
|
205
|
+
) : (
|
|
206
|
+
<Descriptions column={3} size="small" bordered
|
|
207
|
+
style={{marginBottom: 12}}>
|
|
208
|
+
<Descriptions.Item
|
|
209
|
+
label="状态码">{previewResult.status}</Descriptions.Item>
|
|
210
|
+
<Descriptions.Item
|
|
211
|
+
label="耗时">{previewResult.durationMs}ms</Descriptions.Item>
|
|
212
|
+
<Descriptions.Item
|
|
213
|
+
label="响应体大小">{(previewResult.bodyRaw?.length || 0).toLocaleString()} chars</Descriptions.Item>
|
|
214
|
+
</Descriptions>
|
|
215
|
+
)}
|
|
216
|
+
{previewResult.mapped && Object.keys(previewResult.mapped).length > 0 && (
|
|
217
|
+
<Card size="small" title="映射结果" style={{marginBottom: 12}}>
|
|
218
|
+
<pre style={{fontFamily: 'monospace', fontSize: 12, margin: 0, whiteSpace: 'pre-wrap'}}>
|
|
219
|
+
{JSON.stringify(previewResult.mapped, null, 2)}
|
|
220
|
+
</pre>
|
|
221
|
+
</Card>
|
|
222
|
+
)}
|
|
223
|
+
{previewResult.bodyObject && (
|
|
224
|
+
<Card size="small" title="原始响应 (JSON)" style={{marginBottom: 12}}>
|
|
225
|
+
<pre style={{
|
|
226
|
+
fontFamily: 'monospace',
|
|
227
|
+
fontSize: 12,
|
|
228
|
+
margin: 0,
|
|
229
|
+
maxHeight: 300,
|
|
230
|
+
overflow: 'auto',
|
|
231
|
+
whiteSpace: 'pre-wrap'
|
|
232
|
+
}}>
|
|
233
|
+
{JSON.stringify(previewResult.bodyObject, null, 2)}
|
|
234
|
+
</pre>
|
|
235
|
+
</Card>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
),
|
|
241
|
+
},
|
|
242
|
+
]}/>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{/* Step 2: 确认创建 (在当前步骤合并) */}
|
|
247
|
+
{current === 1 && parsed && (
|
|
248
|
+
<div style={{marginTop: 16, textAlign: 'right'}}>
|
|
249
|
+
<Space>
|
|
250
|
+
<Button onClick={() => {
|
|
251
|
+
setCurrent(0);
|
|
252
|
+
setParsed(null)
|
|
253
|
+
}}>返回</Button>
|
|
254
|
+
<Button onClick={handlePreview} loading={previewLoading}>试运行</Button>
|
|
255
|
+
<Button type="primary" onClick={handleCreate} loading={loading}>
|
|
256
|
+
创建脚本
|
|
257
|
+
</Button>
|
|
258
|
+
</Space>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</Modal>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {Button, Input, Select, Space, Switch, Table, Tooltip} from 'antd'
|
|
2
|
+
import {ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined} from '@ant-design/icons'
|
|
3
|
+
|
|
4
|
+
const TYPE_OPTIONS = [
|
|
5
|
+
{value: 'string', label: 'string'},
|
|
6
|
+
{value: 'number', label: 'number'},
|
|
7
|
+
{value: 'boolean', label: 'boolean'},
|
|
8
|
+
{value: 'object', label: 'object'},
|
|
9
|
+
{value: 'array', label: 'array'},
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 字段映射编辑器
|
|
14
|
+
* value: Array<{ name, type, jsonPath, required, description }>
|
|
15
|
+
* onChange: (newList) => void
|
|
16
|
+
*/
|
|
17
|
+
export default function FieldMappingEditor({value = [], onChange, readonly = false}) {
|
|
18
|
+
const update = (index, field, val) => {
|
|
19
|
+
const next = [...value]
|
|
20
|
+
next[index] = {...next[index], [field]: val}
|
|
21
|
+
onChange(next)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const remove = (index) => {
|
|
25
|
+
const next = value.filter((_, i) => i !== index)
|
|
26
|
+
onChange(next)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const move = (index, dir) => {
|
|
30
|
+
const target = index + dir
|
|
31
|
+
if (target < 0 || target >= value.length) return
|
|
32
|
+
const next = [...value]
|
|
33
|
+
const tmp = next[index]
|
|
34
|
+
next[index] = next[target]
|
|
35
|
+
next[target] = tmp
|
|
36
|
+
onChange(next)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const add = () => {
|
|
40
|
+
onChange([...value, {name: '', type: 'string', jsonPath: '', required: false, description: ''}])
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const columns = [
|
|
44
|
+
{
|
|
45
|
+
title: '输出字段名',
|
|
46
|
+
dataIndex: 'name',
|
|
47
|
+
width: 150,
|
|
48
|
+
render: (_v, _r, i) => readonly ? <span style={{fontFamily: 'monospace'}}>{_v}</span> : (
|
|
49
|
+
<Input size="small" value={_v} onChange={e => update(i, 'name', e.target.value)}
|
|
50
|
+
placeholder="e.g. user_name"/>
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: '类型',
|
|
55
|
+
dataIndex: 'type',
|
|
56
|
+
width: 100,
|
|
57
|
+
render: (_v, _r, i) => readonly ? <span>{_v}</span> : (
|
|
58
|
+
<Select size="small" value={_v} onChange={v => update(i, 'type', v)} options={TYPE_OPTIONS}
|
|
59
|
+
style={{width: '100%'}}/>
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: 'JSONPath',
|
|
64
|
+
dataIndex: 'jsonPath',
|
|
65
|
+
width: 220,
|
|
66
|
+
render: (_v, _r, i) => readonly ? <span style={{fontFamily: 'monospace', color: '#0958d9'}}>{_v}</span> : (
|
|
67
|
+
<Input size="small" value={_v} onChange={e => update(i, 'jsonPath', e.target.value)}
|
|
68
|
+
placeholder="$.data.user.name" style={{fontFamily: 'monospace'}}/>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: '必填',
|
|
73
|
+
dataIndex: 'required',
|
|
74
|
+
width: 60,
|
|
75
|
+
render: (_v, _r, i) => readonly ? (
|
|
76
|
+
<span style={{color: _v ? '#ff4d4f' : '#999'}}>{_v ? '是' : '否'}</span>
|
|
77
|
+
) : (
|
|
78
|
+
<Switch size="small" checked={_v} onChange={v => update(i, 'required', v)}/>
|
|
79
|
+
),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: '描述',
|
|
83
|
+
dataIndex: 'description',
|
|
84
|
+
width: 160,
|
|
85
|
+
render: (_v, _r, i) => readonly ? <span style={{color: '#666'}}>{_v}</span> : (
|
|
86
|
+
<Input size="small" value={_v} onChange={e => update(i, 'description', e.target.value)}
|
|
87
|
+
placeholder="字段说明"/>
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
...(readonly ? [] : [{
|
|
91
|
+
title: '操作',
|
|
92
|
+
key: 'action',
|
|
93
|
+
width: 100,
|
|
94
|
+
render: (_v, _r, i) => (
|
|
95
|
+
<Space size="small">
|
|
96
|
+
<Tooltip title="上移"><Button size="small" type="text" icon={<ArrowUpOutlined/>} disabled={i === 0}
|
|
97
|
+
onClick={() => move(i, -1)}/></Tooltip>
|
|
98
|
+
<Tooltip title="下移"><Button size="small" type="text" icon={<ArrowDownOutlined/>}
|
|
99
|
+
disabled={i === value.length - 1}
|
|
100
|
+
onClick={() => move(i, 1)}/></Tooltip>
|
|
101
|
+
<Tooltip title="删除"><Button size="small" type="text" danger icon={<DeleteOutlined/>}
|
|
102
|
+
onClick={() => remove(i)}/></Tooltip>
|
|
103
|
+
</Space>
|
|
104
|
+
),
|
|
105
|
+
}]),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<Table
|
|
111
|
+
dataSource={value}
|
|
112
|
+
columns={columns}
|
|
113
|
+
rowKey={(_, i) => i}
|
|
114
|
+
pagination={false}
|
|
115
|
+
size="small"
|
|
116
|
+
bordered
|
|
117
|
+
style={{marginBottom: 8}}
|
|
118
|
+
/>
|
|
119
|
+
{!readonly && (
|
|
120
|
+
<Button size="small" type="dashed" icon={<PlusOutlined/>} onClick={add} block>
|
|
121
|
+
添加字段映射
|
|
122
|
+
</Button>
|
|
123
|
+
)}
|
|
124
|
+
{value.length === 0 && !readonly && (
|
|
125
|
+
<div style={{textAlign: 'center', color: '#999', padding: 12}}>
|
|
126
|
+
暂未配置输出字段映射。发布后 MCP 工具将返回原始响应体。
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|