@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.
Files changed (41) hide show
  1. package/README.md +66 -0
  2. package/dist/z-agent-frontend-component.css +1 -0
  3. package/dist/z-agent-frontend-component.es.js +9956 -0
  4. package/dist/z-agent-frontend-component.umd.js +219 -0
  5. package/package.json +77 -0
  6. package/src/api/apiRouter.js +78 -0
  7. package/src/api/index.js +23 -0
  8. package/src/api/request.js +59 -0
  9. package/src/api/routes.js +140 -0
  10. package/src/dev.jsx +80 -0
  11. package/src/index.js +86 -0
  12. package/src/pages/agent/app/index.jsx +2 -0
  13. package/src/pages/agent/editor/AgentAppEditor.jsx +456 -0
  14. package/src/pages/agent/editor/WorkflowEditor.jsx +495 -0
  15. package/src/pages/agent/editor/nodes/index.ts +225 -0
  16. package/src/pages/agent/index.jsx +1379 -0
  17. package/src/pages/agent/share.jsx +512 -0
  18. package/src/pages/ak/AkUsageDrawer.jsx +208 -0
  19. package/src/pages/ak/index.jsx +496 -0
  20. package/src/pages/llm/index.jsx +736 -0
  21. package/src/pages/llm/model/index.jsx +220 -0
  22. package/src/pages/llm/provider/index.jsx +173 -0
  23. package/src/pages/mcp/index.jsx +359 -0
  24. package/src/pages/oss/BucketList.jsx +320 -0
  25. package/src/pages/oss/ObjectBrowser.jsx +409 -0
  26. package/src/pages/product/execute.jsx +608 -0
  27. package/src/pages/product/index.jsx +628 -0
  28. package/src/pages/product/scene.jsx +746 -0
  29. package/src/pages/script/ApiBridgeEditor.jsx +255 -0
  30. package/src/pages/script/CurlImportModal.jsx +263 -0
  31. package/src/pages/script/FieldMappingEditor.jsx +131 -0
  32. package/src/pages/script/OpenApiImportModal.jsx +212 -0
  33. package/src/pages/script/index.jsx +532 -0
  34. package/src/pages/skill/index.jsx +1595 -0
  35. package/src/pages/trace/DebugPlayground.jsx +357 -0
  36. package/src/pages/trace/components/MetricsDashboard.jsx +164 -0
  37. package/src/pages/trace/components/RagFragments.jsx +134 -0
  38. package/src/pages/trace/components/Timeline.jsx +142 -0
  39. package/src/pages/trace/components/ToolCallTree.jsx +116 -0
  40. package/src/pages/trace/index.jsx +13 -0
  41. 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
+ }