@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,736 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
Col,
|
|
6
|
+
Divider,
|
|
7
|
+
Empty,
|
|
8
|
+
Form,
|
|
9
|
+
Input,
|
|
10
|
+
InputNumber,
|
|
11
|
+
message,
|
|
12
|
+
Modal,
|
|
13
|
+
Row,
|
|
14
|
+
Select,
|
|
15
|
+
Space,
|
|
16
|
+
Spin,
|
|
17
|
+
Tag,
|
|
18
|
+
Tooltip
|
|
19
|
+
} from 'antd'
|
|
20
|
+
import {
|
|
21
|
+
ApiOutlined,
|
|
22
|
+
AudioOutlined,
|
|
23
|
+
BranchesOutlined,
|
|
24
|
+
DeleteOutlined,
|
|
25
|
+
EditOutlined,
|
|
26
|
+
EyeOutlined,
|
|
27
|
+
GlobalOutlined,
|
|
28
|
+
NodeIndexOutlined,
|
|
29
|
+
PictureOutlined,
|
|
30
|
+
PlusOutlined,
|
|
31
|
+
ReloadOutlined,
|
|
32
|
+
ThunderboltOutlined
|
|
33
|
+
} from '@ant-design/icons'
|
|
34
|
+
import request from '../../api'
|
|
35
|
+
|
|
36
|
+
const {confirm} = Modal
|
|
37
|
+
|
|
38
|
+
// 示例数据
|
|
39
|
+
const mockProviders = [
|
|
40
|
+
{
|
|
41
|
+
providerCode: 'ollama',
|
|
42
|
+
providerName: 'Ollama',
|
|
43
|
+
providerType: 'ollama',
|
|
44
|
+
baseUrl: 'http://localhost:11434',
|
|
45
|
+
enabled: 1,
|
|
46
|
+
priority: 100
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
providerCode: 'openai',
|
|
50
|
+
providerName: 'OpenAI',
|
|
51
|
+
providerType: 'openai',
|
|
52
|
+
baseUrl: 'https://api.openai.com',
|
|
53
|
+
enabled: 1,
|
|
54
|
+
priority: 90
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
providerCode: 'dashscope',
|
|
58
|
+
providerName: '阿里通义',
|
|
59
|
+
providerType: 'dashscope',
|
|
60
|
+
baseUrl: 'https://dashscope.aliyuncs.com',
|
|
61
|
+
enabled: 1,
|
|
62
|
+
priority: 80
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const mockModels = [
|
|
67
|
+
{
|
|
68
|
+
id: 1,
|
|
69
|
+
modelCode: 'qwen2.5:7b-instruct-q4_K_M',
|
|
70
|
+
modelName: 'Qwen 2.5 7B',
|
|
71
|
+
providerCode: 'ollama',
|
|
72
|
+
modelType: 'chat',
|
|
73
|
+
contextWindow: 32768,
|
|
74
|
+
status: 'ACTIVE',
|
|
75
|
+
description: '通义千问 2.5 轻量级文本生成模型',
|
|
76
|
+
inputPrice: 0,
|
|
77
|
+
outputPrice: 0
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 2,
|
|
81
|
+
modelCode: 'qwen3:8b',
|
|
82
|
+
modelName: 'Qwen3 8B',
|
|
83
|
+
providerCode: 'ollama',
|
|
84
|
+
modelType: 'chat',
|
|
85
|
+
contextWindow: 40960,
|
|
86
|
+
status: 'ACTIVE',
|
|
87
|
+
description: '通义千问 3 最新一代模型',
|
|
88
|
+
inputPrice: 0,
|
|
89
|
+
outputPrice: 0
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 3,
|
|
93
|
+
modelCode: 'gpt-4o',
|
|
94
|
+
modelName: 'GPT-4o',
|
|
95
|
+
providerCode: 'openai',
|
|
96
|
+
modelType: 'chat',
|
|
97
|
+
contextWindow: 128000,
|
|
98
|
+
status: 'ACTIVE',
|
|
99
|
+
description: 'OpenAI 旗舰多模态模型',
|
|
100
|
+
inputPrice: 0.0025,
|
|
101
|
+
outputPrice: 0.01
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 4,
|
|
105
|
+
modelCode: 'gpt-4o-mini',
|
|
106
|
+
modelName: 'GPT-4o Mini',
|
|
107
|
+
providerCode: 'openai',
|
|
108
|
+
modelType: 'chat',
|
|
109
|
+
contextWindow: 128000,
|
|
110
|
+
status: 'ACTIVE',
|
|
111
|
+
description: '轻量快速,性价比高',
|
|
112
|
+
inputPrice: 0.00015,
|
|
113
|
+
outputPrice: 0.0006
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 5,
|
|
117
|
+
modelCode: 'qwen-vl-max',
|
|
118
|
+
modelName: 'Qwen VL Max',
|
|
119
|
+
providerCode: 'dashscope',
|
|
120
|
+
modelType: 'vision',
|
|
121
|
+
contextWindow: 8192,
|
|
122
|
+
status: 'ACTIVE',
|
|
123
|
+
description: '通义千问视觉模型,支持图像理解',
|
|
124
|
+
inputPrice: 0.002,
|
|
125
|
+
outputPrice: 0.008
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 6,
|
|
129
|
+
modelCode: 'qwen2-audio',
|
|
130
|
+
modelName: 'Qwen2 Audio',
|
|
131
|
+
providerCode: 'dashscope',
|
|
132
|
+
modelType: 'audio',
|
|
133
|
+
contextWindow: 8192,
|
|
134
|
+
status: 'ACTIVE',
|
|
135
|
+
description: '通义千问语音模型',
|
|
136
|
+
inputPrice: 0.001,
|
|
137
|
+
outputPrice: 0.004
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 7,
|
|
141
|
+
modelCode: 'text-embedding-v3',
|
|
142
|
+
modelName: 'Text Embedding V3',
|
|
143
|
+
providerCode: 'openai',
|
|
144
|
+
modelType: 'embedding',
|
|
145
|
+
contextWindow: 8191,
|
|
146
|
+
status: 'ACTIVE',
|
|
147
|
+
description: 'OpenAI 文本嵌入模型',
|
|
148
|
+
inputPrice: 0.00002,
|
|
149
|
+
outputPrice: 0
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 8,
|
|
153
|
+
modelCode: 'codestral',
|
|
154
|
+
modelName: 'Codestral',
|
|
155
|
+
providerCode: 'ollama',
|
|
156
|
+
modelType: 'code',
|
|
157
|
+
contextWindow: 32768,
|
|
158
|
+
status: 'ACTIVE',
|
|
159
|
+
description: '代码生成专用模型,支持多种编程语言',
|
|
160
|
+
inputPrice: 0,
|
|
161
|
+
outputPrice: 0
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 9,
|
|
165
|
+
modelCode: 'dall-e-3',
|
|
166
|
+
modelName: 'DALL·E 3',
|
|
167
|
+
providerCode: 'openai',
|
|
168
|
+
modelType: 'image',
|
|
169
|
+
contextWindow: 0,
|
|
170
|
+
status: 'ACTIVE',
|
|
171
|
+
description: 'OpenAI 图像生成模型',
|
|
172
|
+
inputPrice: 0.04,
|
|
173
|
+
outputPrice: 0
|
|
174
|
+
},
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
// ===== 能力分类定义(阿里云风格) =====
|
|
178
|
+
const CATEGORY_DEFS = [
|
|
179
|
+
{key: 'chat', title: '文本生成', icon: <BranchesOutlined/>, color: '#1677ff', bg: '#e6f4ff', border: '#1677ff'},
|
|
180
|
+
{key: 'code', title: '代码生成', icon: <ThunderboltOutlined/>, color: '#722ed1', bg: '#f9f0ff', border: '#722ed1'},
|
|
181
|
+
{key: 'image', title: '图像生成', icon: <PictureOutlined/>, color: '#13c2c2', bg: '#e6fffb', border: '#13c2c2'},
|
|
182
|
+
{key: 'vision', title: '视觉理解', icon: <EyeOutlined/>, color: '#fa8c16', bg: '#fff7e6', border: '#fa8c16'},
|
|
183
|
+
{key: 'audio', title: '语音', icon: <AudioOutlined/>, color: '#eb2f96', bg: '#fff0f6', border: '#eb2f96'},
|
|
184
|
+
{
|
|
185
|
+
key: 'embedding',
|
|
186
|
+
title: '嵌入/向量',
|
|
187
|
+
icon: <NodeIndexOutlined/>,
|
|
188
|
+
color: '#52c41a',
|
|
189
|
+
bg: '#f6ffed',
|
|
190
|
+
border: '#52c41a'
|
|
191
|
+
},
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
// 标准供应商 baseUrl 自动填充
|
|
195
|
+
const STANDARD_BASE_URLS = {
|
|
196
|
+
openai: 'https://api.openai.com',
|
|
197
|
+
dashscope: 'https://dashscope.aliyuncs.com',
|
|
198
|
+
azure: 'https://{resource}.openai.azure.com',
|
|
199
|
+
anthropic: 'https://api.anthropic.com',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const providerTypeList = [
|
|
203
|
+
{value: 'ollama', label: 'Ollama'},
|
|
204
|
+
{value: 'openai', label: 'OpenAI'},
|
|
205
|
+
{value: 'dashscope', label: '阿里通义'},
|
|
206
|
+
{value: 'azure', label: 'Azure OpenAI'},
|
|
207
|
+
{value: 'anthropic', label: 'Anthropic'},
|
|
208
|
+
{value: 'custom', label: '自定义'},
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
const ModelManage = () => {
|
|
212
|
+
const [models, setModels] = useState([])
|
|
213
|
+
const [providers, setProviders] = useState([])
|
|
214
|
+
const [loading, setLoading] = useState(false)
|
|
215
|
+
const [selectedCategory, setSelectedCategory] = useState('chat')
|
|
216
|
+
const [selectedProvider, setSelectedProvider] = useState(null)
|
|
217
|
+
const [modalVisible, setModalVisible] = useState(false)
|
|
218
|
+
const [editing, setEditing] = useState(null)
|
|
219
|
+
const [form] = Form.useForm()
|
|
220
|
+
const [providerModalVisible, setProviderModalVisible] = useState(false)
|
|
221
|
+
const [editingProvider, setEditingProvider] = useState(null)
|
|
222
|
+
const [providerForm] = Form.useForm()
|
|
223
|
+
|
|
224
|
+
const fetchData = async () => {
|
|
225
|
+
setLoading(true)
|
|
226
|
+
try {
|
|
227
|
+
const [mRes, pRes] = await Promise.all([
|
|
228
|
+
request('/llm-center/model/list', {method: 'GET'}).catch(() => null),
|
|
229
|
+
request('/llm-center/provider/list', {method: 'GET'}).catch(() => null),
|
|
230
|
+
])
|
|
231
|
+
const modelsData = Array.isArray(mRes) ? mRes : (mRes?.data || [])
|
|
232
|
+
const providersData = Array.isArray(pRes) ? pRes : (pRes?.data || [])
|
|
233
|
+
if (modelsData.length === 0 && providersData.length === 0) {
|
|
234
|
+
setProviders(mockProviders)
|
|
235
|
+
setModels(mockModels)
|
|
236
|
+
} else {
|
|
237
|
+
setModels(modelsData)
|
|
238
|
+
setProviders(providersData)
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
setProviders(mockProviders)
|
|
242
|
+
setModels(mockModels)
|
|
243
|
+
} finally {
|
|
244
|
+
setLoading(false)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
fetchData()
|
|
250
|
+
}, [])
|
|
251
|
+
|
|
252
|
+
// 各分类模型数
|
|
253
|
+
const categoryCounts = {}
|
|
254
|
+
CATEGORY_DEFS.forEach(c => {
|
|
255
|
+
categoryCounts[c.key] = models.filter(m => m.modelType === c.key).length
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// 筛选模型:按 modelType 精确匹配分类 + 供应商
|
|
259
|
+
const filteredModels = models.filter(m => {
|
|
260
|
+
const catMatch = !selectedCategory || m.modelType === selectedCategory
|
|
261
|
+
const provMatch = !selectedProvider || m.providerCode === selectedProvider
|
|
262
|
+
return catMatch && provMatch
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const providerTypeColor = {
|
|
266
|
+
ollama: 'blue', openai: 'green', dashscope: 'cyan', azure: 'purple', anthropic: 'orange', custom: 'default',
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ===== 模型 CRUD =====
|
|
270
|
+
const handleAddModel = () => {
|
|
271
|
+
setEditing(null)
|
|
272
|
+
form.resetFields()
|
|
273
|
+
form.setFieldsValue({status: 'ACTIVE', contextWindow: 4096})
|
|
274
|
+
setModalVisible(true)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const handleEditModel = (record) => {
|
|
278
|
+
setEditing(record)
|
|
279
|
+
form.setFieldsValue(record)
|
|
280
|
+
setModalVisible(true)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const handleDeleteModel = (record) => {
|
|
284
|
+
confirm({
|
|
285
|
+
title: '确认删除',
|
|
286
|
+
content: `删除模型 ${record.modelName} ?`,
|
|
287
|
+
onOk: async () => {
|
|
288
|
+
if (models === mockModels) {
|
|
289
|
+
setModels(models.filter(m => m.id !== record.id))
|
|
290
|
+
message.success('删除成功')
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
await request('/llm-center/model/delete', {method: 'POST', data: {id: record.id}})
|
|
294
|
+
message.success('删除成功')
|
|
295
|
+
fetchData()
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const handleModelSubmit = async () => {
|
|
301
|
+
try {
|
|
302
|
+
const values = await form.validateFields()
|
|
303
|
+
if (models === mockModels) {
|
|
304
|
+
if (editing) {
|
|
305
|
+
setModels(models.map(m => m.id === editing.id ? {...m, ...values} : m))
|
|
306
|
+
} else {
|
|
307
|
+
setModels([...models, {...values, id: Date.now()}])
|
|
308
|
+
}
|
|
309
|
+
message.success(editing ? '更新成功' : '创建成功')
|
|
310
|
+
setModalVisible(false)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
await request(editing ? '/llm-center/model/update' : '/llm-center/model/create', {
|
|
314
|
+
method: 'POST', data: values
|
|
315
|
+
})
|
|
316
|
+
message.success(editing ? '更新成功' : '创建成功')
|
|
317
|
+
setModalVisible(false)
|
|
318
|
+
fetchData()
|
|
319
|
+
} catch (e) {
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ===== 供应商 CRUD =====
|
|
324
|
+
const handleAddProvider = () => {
|
|
325
|
+
setEditingProvider(null)
|
|
326
|
+
providerForm.resetFields()
|
|
327
|
+
providerForm.setFieldsValue({status: 'ACTIVE'})
|
|
328
|
+
setProviderModalVisible(true)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const handleEditProvider = (record) => {
|
|
332
|
+
setEditingProvider(record)
|
|
333
|
+
providerForm.setFieldsValue({
|
|
334
|
+
...record,
|
|
335
|
+
apiKey: record.apiKey ? '••••••••' : '',
|
|
336
|
+
})
|
|
337
|
+
setProviderModalVisible(true)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const handleDeleteProvider = (record) => {
|
|
341
|
+
confirm({
|
|
342
|
+
title: '确认删除',
|
|
343
|
+
content: `删除供应商 ${record.providerName} ?`,
|
|
344
|
+
onOk: async () => {
|
|
345
|
+
if (providers === mockProviders) {
|
|
346
|
+
setProviders(providers.filter(p => p.providerCode !== record.providerCode))
|
|
347
|
+
message.success('删除成功')
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
await request('/llm-center/provider/delete', {method: 'POST', data: {id: record.id}})
|
|
351
|
+
message.success('删除成功')
|
|
352
|
+
fetchData()
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const handleProviderSubmit = async () => {
|
|
358
|
+
try {
|
|
359
|
+
const values = await providerForm.validateFields()
|
|
360
|
+
if (providers === mockProviders) {
|
|
361
|
+
if (editingProvider) {
|
|
362
|
+
setProviders(providers.map(p => p.providerCode === editingProvider.providerCode ? {...p, ...values} : p))
|
|
363
|
+
} else {
|
|
364
|
+
setProviders([...providers, {...values}])
|
|
365
|
+
}
|
|
366
|
+
message.success(editingProvider ? '更新成功' : '创建成功')
|
|
367
|
+
setProviderModalVisible(false)
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
await request(editingProvider ? '/llm-center/provider/update' : '/llm-center/provider/create', {
|
|
371
|
+
method: 'POST', data: values
|
|
372
|
+
})
|
|
373
|
+
message.success(editingProvider ? '更新成功' : '创建成功')
|
|
374
|
+
setProviderModalVisible(false)
|
|
375
|
+
fetchData()
|
|
376
|
+
} catch (e) {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 阿里云风格分类树渲染
|
|
381
|
+
const renderCategoryItem = (cat) => {
|
|
382
|
+
const selected = selectedCategory === cat.key
|
|
383
|
+
const count = categoryCounts[cat.key] || 0
|
|
384
|
+
return (
|
|
385
|
+
<div
|
|
386
|
+
key={cat.key}
|
|
387
|
+
onClick={() => setSelectedCategory(cat.key)}
|
|
388
|
+
style={{
|
|
389
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
390
|
+
padding: '10px 14px',
|
|
391
|
+
marginBottom: 2,
|
|
392
|
+
cursor: 'pointer',
|
|
393
|
+
borderRadius: 8,
|
|
394
|
+
borderLeft: selected ? `3px solid ${cat.color}` : '3px solid transparent',
|
|
395
|
+
background: selected ? cat.bg : 'transparent',
|
|
396
|
+
transition: 'all 0.2s',
|
|
397
|
+
}}
|
|
398
|
+
onMouseEnter={(e) => {
|
|
399
|
+
if (!selected) e.currentTarget.style.background = '#f5f5f5'
|
|
400
|
+
}}
|
|
401
|
+
onMouseLeave={(e) => {
|
|
402
|
+
if (!selected) e.currentTarget.style.background = 'transparent'
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
<div style={{
|
|
406
|
+
width: 32, height: 32, borderRadius: 8,
|
|
407
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
408
|
+
background: selected ? cat.color : '#f0f0f0',
|
|
409
|
+
color: selected ? '#fff' : '#8c8c8c',
|
|
410
|
+
fontSize: 16,
|
|
411
|
+
flexShrink: 0,
|
|
412
|
+
transition: 'all 0.2s',
|
|
413
|
+
}}>
|
|
414
|
+
{cat.icon}
|
|
415
|
+
</div>
|
|
416
|
+
<div style={{flex: 1, minWidth: 0}}>
|
|
417
|
+
<div style={{
|
|
418
|
+
fontSize: 13, fontWeight: selected ? 600 : 400,
|
|
419
|
+
color: selected ? '#262626' : '#595959',
|
|
420
|
+
lineHeight: 1.4,
|
|
421
|
+
}}>
|
|
422
|
+
{cat.title}
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
<span style={{
|
|
426
|
+
fontSize: 11, color: selected ? cat.color : '#bababa',
|
|
427
|
+
fontWeight: selected ? 600 : 400,
|
|
428
|
+
background: selected ? '#fff' : 'transparent',
|
|
429
|
+
padding: '1px 7px',
|
|
430
|
+
borderRadius: 10,
|
|
431
|
+
}}>
|
|
432
|
+
{count}
|
|
433
|
+
</span>
|
|
434
|
+
</div>
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const currentCat = CATEGORY_DEFS.find(c => c.key === selectedCategory) || CATEGORY_DEFS[0]
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<div style={{display: 'flex', gap: 16, height: 'calc(100vh - 180px)', padding: 16}}>
|
|
442
|
+
{/* 左侧:能力分类 — 阿里云风格 */}
|
|
443
|
+
<Card
|
|
444
|
+
size="small"
|
|
445
|
+
style={{
|
|
446
|
+
width: 210,
|
|
447
|
+
overflow: 'auto',
|
|
448
|
+
flexShrink: 0,
|
|
449
|
+
border: 'none',
|
|
450
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.06)'
|
|
451
|
+
}}
|
|
452
|
+
bodyStyle={{padding: '8px 0'}}
|
|
453
|
+
>
|
|
454
|
+
<div style={{padding: '8px 14px 4px', fontSize: 12, color: '#8c8c8c', fontWeight: 500}}>
|
|
455
|
+
能力分类
|
|
456
|
+
</div>
|
|
457
|
+
{CATEGORY_DEFS.map(renderCategoryItem)}
|
|
458
|
+
</Card>
|
|
459
|
+
|
|
460
|
+
{/* 右侧:供应商标签 + 模型卡片 */}
|
|
461
|
+
<div style={{flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 12}}>
|
|
462
|
+
{/* 供应商标签栏 */}
|
|
463
|
+
<Card size="small" style={{flexShrink: 0}} bodyStyle={{padding: '10px 16px'}}>
|
|
464
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap'}}>
|
|
465
|
+
<span
|
|
466
|
+
style={{fontSize: 12, color: '#8c8c8c', marginRight: 4, whiteSpace: 'nowrap'}}>供应商</span>
|
|
467
|
+
<Tag
|
|
468
|
+
style={{cursor: 'pointer', padding: '2px 10px', fontSize: 12, borderRadius: 16, margin: 0}}
|
|
469
|
+
color={!selectedProvider ? 'blue' : 'default'}
|
|
470
|
+
onClick={() => setSelectedProvider(null)}
|
|
471
|
+
>全部</Tag>
|
|
472
|
+
{providers.map(p => (
|
|
473
|
+
<Tag
|
|
474
|
+
key={p.providerCode}
|
|
475
|
+
style={{
|
|
476
|
+
cursor: 'pointer',
|
|
477
|
+
padding: '2px 10px',
|
|
478
|
+
fontSize: 12,
|
|
479
|
+
borderRadius: 16,
|
|
480
|
+
margin: 0
|
|
481
|
+
}}
|
|
482
|
+
color={selectedProvider === p.providerCode ? (providerTypeColor[p.providerType] || 'blue') : 'default'}
|
|
483
|
+
onClick={() => setSelectedProvider(p.providerCode)}
|
|
484
|
+
>
|
|
485
|
+
{p.providerName}
|
|
486
|
+
</Tag>
|
|
487
|
+
))}
|
|
488
|
+
<div style={{flex: 1}}/>
|
|
489
|
+
<Button size="small" type="primary" ghost icon={<PlusOutlined/>}
|
|
490
|
+
onClick={handleAddModel}>新增模型</Button>
|
|
491
|
+
<Button size="small" icon={<ApiOutlined/>} onClick={handleAddProvider}>新增供应商</Button>
|
|
492
|
+
<Button size="small" icon={<ReloadOutlined/>} onClick={fetchData}/>
|
|
493
|
+
</div>
|
|
494
|
+
</Card>
|
|
495
|
+
|
|
496
|
+
{/* 模型卡片 */}
|
|
497
|
+
<div style={{flex: 1, overflow: 'auto'}}>
|
|
498
|
+
{loading ? (
|
|
499
|
+
<div style={{textAlign: 'center', padding: 80}}><Spin size="large"/></div>
|
|
500
|
+
) : filteredModels.length === 0 ? (
|
|
501
|
+
<div style={{textAlign: 'center', padding: 80}}><Empty description="暂无匹配的模型"/></div>
|
|
502
|
+
) : (
|
|
503
|
+
<div style={{
|
|
504
|
+
display: 'grid',
|
|
505
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))',
|
|
506
|
+
gap: 12
|
|
507
|
+
}}>
|
|
508
|
+
{filteredModels.map(model => {
|
|
509
|
+
const provider = providers.find(p => p.providerCode === model.providerCode)
|
|
510
|
+
const catDef = CATEGORY_DEFS.find(c => c.key === model.modelType)
|
|
511
|
+
return (
|
|
512
|
+
<Card
|
|
513
|
+
key={model.id || model.modelCode}
|
|
514
|
+
size="small"
|
|
515
|
+
hoverable
|
|
516
|
+
style={{borderRadius: 8, borderTop: `3px solid ${catDef?.color || '#d9d9d9'}`}}
|
|
517
|
+
bodyStyle={{padding: 14}}
|
|
518
|
+
actions={[
|
|
519
|
+
<Tooltip key="edit" title="编辑模型"><EditOutlined
|
|
520
|
+
onClick={() => handleEditModel(model)}/></Tooltip>,
|
|
521
|
+
<Tooltip key="delete" title="删除模型"><DeleteOutlined
|
|
522
|
+
style={{color: '#ff4d4f'}}
|
|
523
|
+
onClick={() => handleDeleteModel(model)}/></Tooltip>,
|
|
524
|
+
]}
|
|
525
|
+
>
|
|
526
|
+
<div style={{display: 'flex', gap: 12}}>
|
|
527
|
+
<div style={{
|
|
528
|
+
width: 44,
|
|
529
|
+
height: 44,
|
|
530
|
+
borderRadius: 10,
|
|
531
|
+
background: catDef ? `linear-gradient(135deg, ${catDef.color}, ${catDef.color}dd)` : '#f0f0f0',
|
|
532
|
+
display: 'flex',
|
|
533
|
+
alignItems: 'center',
|
|
534
|
+
justifyContent: 'center',
|
|
535
|
+
flexShrink: 0,
|
|
536
|
+
}}>
|
|
537
|
+
{catDef ? (
|
|
538
|
+
<span style={{color: '#fff', fontSize: 20}}>{catDef.icon}</span>
|
|
539
|
+
) : (
|
|
540
|
+
<GlobalOutlined style={{fontSize: 20, color: '#fff'}}/>
|
|
541
|
+
)}
|
|
542
|
+
</div>
|
|
543
|
+
<div style={{flex: 1, minWidth: 0}}>
|
|
544
|
+
<div style={{
|
|
545
|
+
display: 'flex',
|
|
546
|
+
justifyContent: 'space-between',
|
|
547
|
+
alignItems: 'flex-start'
|
|
548
|
+
}}>
|
|
549
|
+
<div>
|
|
550
|
+
<div style={{
|
|
551
|
+
fontWeight: 600,
|
|
552
|
+
fontSize: 14,
|
|
553
|
+
color: '#262626'
|
|
554
|
+
}}>{model.modelName || model.modelCode}</div>
|
|
555
|
+
<div style={{
|
|
556
|
+
fontSize: 12,
|
|
557
|
+
color: '#8c8c8c',
|
|
558
|
+
fontFamily: 'monospace'
|
|
559
|
+
}}>{model.modelCode}</div>
|
|
560
|
+
</div>
|
|
561
|
+
<Tag color={model.status === 'ACTIVE' ? 'green' : 'red'} style={{
|
|
562
|
+
borderRadius: 20,
|
|
563
|
+
fontSize: 11,
|
|
564
|
+
lineHeight: '20px',
|
|
565
|
+
margin: 0
|
|
566
|
+
}}>
|
|
567
|
+
{model.status === 'ACTIVE' ? '正常' : '禁用'}
|
|
568
|
+
</Tag>
|
|
569
|
+
</div>
|
|
570
|
+
<div style={{marginTop: 8, display: 'flex', gap: 6, flexWrap: 'wrap'}}>
|
|
571
|
+
<Tag color={providerTypeColor[provider?.providerType] || 'default'}
|
|
572
|
+
style={{fontSize: 11, borderRadius: 4}}>
|
|
573
|
+
<GlobalOutlined style={{marginRight: 4}}/>
|
|
574
|
+
{provider?.providerName || model.providerCode}
|
|
575
|
+
</Tag>
|
|
576
|
+
<Tag style={{fontSize: 11, borderRadius: 4}}>
|
|
577
|
+
{CATEGORY_DEFS.find(c => c.key === model.modelType)?.title || model.modelType}
|
|
578
|
+
</Tag>
|
|
579
|
+
{model.contextWindow > 0 && (
|
|
580
|
+
<Tag style={{fontSize: 11, borderRadius: 4}}>
|
|
581
|
+
{(model.contextWindow / 1000).toFixed(0)}K CTX
|
|
582
|
+
</Tag>
|
|
583
|
+
)}
|
|
584
|
+
</div>
|
|
585
|
+
{model.description && (
|
|
586
|
+
<div style={{
|
|
587
|
+
marginTop: 6,
|
|
588
|
+
fontSize: 12,
|
|
589
|
+
color: '#8c8c8c',
|
|
590
|
+
lineHeight: 1.5
|
|
591
|
+
}}>
|
|
592
|
+
{model.description}
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
</Card>
|
|
598
|
+
)
|
|
599
|
+
})}
|
|
600
|
+
</div>
|
|
601
|
+
)}
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
{/* 模型编辑弹窗 */}
|
|
606
|
+
<Modal title={editing ? '编辑模型' : '新增模型'} open={modalVisible}
|
|
607
|
+
onOk={handleModelSubmit} onCancel={() => setModalVisible(false)} width={640}>
|
|
608
|
+
<Form form={form} layout="vertical">
|
|
609
|
+
<Row gutter={16}>
|
|
610
|
+
<Col span={12}>
|
|
611
|
+
<Form.Item name="modelCode" label="模型编码" rules={[{required: true}]}>
|
|
612
|
+
<Input placeholder="如: qwen2.5:72b, gpt-4o" disabled={!!editing}/>
|
|
613
|
+
</Form.Item>
|
|
614
|
+
</Col>
|
|
615
|
+
<Col span={12}>
|
|
616
|
+
<Form.Item name="modelName" label="模型名称" rules={[{required: true}]}>
|
|
617
|
+
<Input placeholder="如: Qwen 2.5 72B"/>
|
|
618
|
+
</Form.Item>
|
|
619
|
+
</Col>
|
|
620
|
+
</Row>
|
|
621
|
+
<Row gutter={16}>
|
|
622
|
+
<Col span={12}>
|
|
623
|
+
<Form.Item name="providerCode" label="所属供应商" rules={[{required: true}]}>
|
|
624
|
+
<Select placeholder="选择供应商">
|
|
625
|
+
{providers.map(p => (
|
|
626
|
+
<Select.Option key={p.providerCode}
|
|
627
|
+
value={p.providerCode}>{p.providerName}</Select.Option>
|
|
628
|
+
))}
|
|
629
|
+
</Select>
|
|
630
|
+
</Form.Item>
|
|
631
|
+
</Col>
|
|
632
|
+
<Col span={12}>
|
|
633
|
+
<Form.Item name="modelType" label="能力类型" rules={[{required: true}]}>
|
|
634
|
+
<Select placeholder="选择能力分类">
|
|
635
|
+
{CATEGORY_DEFS.map(c => (
|
|
636
|
+
<Select.Option key={c.key} value={c.key}>
|
|
637
|
+
<Space><span style={{color: c.color}}>{c.icon}</span>{c.title}</Space>
|
|
638
|
+
</Select.Option>
|
|
639
|
+
))}
|
|
640
|
+
</Select>
|
|
641
|
+
</Form.Item>
|
|
642
|
+
</Col>
|
|
643
|
+
</Row>
|
|
644
|
+
<Row gutter={16}>
|
|
645
|
+
<Col span={12}>
|
|
646
|
+
<Form.Item name="contextWindow" label="上下文窗口 (tokens)">
|
|
647
|
+
<InputNumber style={{width: '100%'}} placeholder="如: 4096" min={0}/>
|
|
648
|
+
</Form.Item>
|
|
649
|
+
</Col>
|
|
650
|
+
<Col span={12}>
|
|
651
|
+
<Form.Item name="status" label="状态" initialValue="ACTIVE">
|
|
652
|
+
<Select>
|
|
653
|
+
<Select.Option value="ACTIVE">正常</Select.Option>
|
|
654
|
+
<Select.Option value="INACTIVE">禁用</Select.Option>
|
|
655
|
+
</Select>
|
|
656
|
+
</Form.Item>
|
|
657
|
+
</Col>
|
|
658
|
+
</Row>
|
|
659
|
+
<Row gutter={16}>
|
|
660
|
+
<Col span={12}>
|
|
661
|
+
<Form.Item name="inputPrice" label="输入价格 (元/千token)">
|
|
662
|
+
<InputNumber style={{width: '100%'}} min={0} precision={6}/>
|
|
663
|
+
</Form.Item>
|
|
664
|
+
</Col>
|
|
665
|
+
<Col span={12}>
|
|
666
|
+
<Form.Item name="outputPrice" label="输出价格 (元/千token)">
|
|
667
|
+
<InputNumber style={{width: '100%'}} min={0} precision={6}/>
|
|
668
|
+
</Form.Item>
|
|
669
|
+
</Col>
|
|
670
|
+
</Row>
|
|
671
|
+
<Form.Item name="description" label="描述">
|
|
672
|
+
<Input.TextArea rows={2} placeholder="模型描述..."/>
|
|
673
|
+
</Form.Item>
|
|
674
|
+
</Form>
|
|
675
|
+
</Modal>
|
|
676
|
+
|
|
677
|
+
{/* 供应商编辑弹窗 */}
|
|
678
|
+
<Modal title={editingProvider ? '编辑供应商' : '新增供应商'} open={providerModalVisible}
|
|
679
|
+
onOk={handleProviderSubmit} onCancel={() => setProviderModalVisible(false)} width={560}>
|
|
680
|
+
<Form form={providerForm} layout="vertical">
|
|
681
|
+
<Row gutter={16}>
|
|
682
|
+
<Col span={12}>
|
|
683
|
+
<Form.Item name="providerCode" label="供应商编码" rules={[{required: true}]}>
|
|
684
|
+
<Input placeholder="如: ollama, openai" disabled={!!editingProvider}/>
|
|
685
|
+
</Form.Item>
|
|
686
|
+
</Col>
|
|
687
|
+
<Col span={12}>
|
|
688
|
+
<Form.Item name="providerName" label="供应商名称" rules={[{required: true}]}>
|
|
689
|
+
<Input placeholder="如: Ollama, OpenAI"/>
|
|
690
|
+
</Form.Item>
|
|
691
|
+
</Col>
|
|
692
|
+
</Row>
|
|
693
|
+
<Row gutter={16}>
|
|
694
|
+
<Col span={12}>
|
|
695
|
+
<Form.Item name="providerType" label="类型" rules={[{required: true}]}>
|
|
696
|
+
<Select placeholder="选择类型">
|
|
697
|
+
{providerTypeList.map(t => (
|
|
698
|
+
<Select.Option key={t.value} value={t.value}>{t.label}</Select.Option>
|
|
699
|
+
))}
|
|
700
|
+
</Select>
|
|
701
|
+
</Form.Item>
|
|
702
|
+
</Col>
|
|
703
|
+
<Col span={12}>
|
|
704
|
+
<Form.Item name="status" label="状态" initialValue="ACTIVE">
|
|
705
|
+
<Select>
|
|
706
|
+
<Select.Option value="ACTIVE">正常</Select.Option>
|
|
707
|
+
<Select.Option value="INACTIVE">禁用</Select.Option>
|
|
708
|
+
</Select>
|
|
709
|
+
</Form.Item>
|
|
710
|
+
</Col>
|
|
711
|
+
</Row>
|
|
712
|
+
|
|
713
|
+
<Divider style={{margin: '8px 0', fontSize: 12, color: '#8c8c8c'}}>连接配置</Divider>
|
|
714
|
+
|
|
715
|
+
<Form.Item
|
|
716
|
+
name="baseUrl"
|
|
717
|
+
label="Base URL"
|
|
718
|
+
rules={[{required: true}]}
|
|
719
|
+
tooltip="标准供应商自动填充地址;自定义/Ollama 需手动填写"
|
|
720
|
+
>
|
|
721
|
+
<Input placeholder="如: http://localhost:11434"/>
|
|
722
|
+
</Form.Item>
|
|
723
|
+
<Form.Item
|
|
724
|
+
name="apiKey"
|
|
725
|
+
label="API Key"
|
|
726
|
+
tooltip="系统级凭据,可在系统配置中统一管理。此字段仅用于展示,实际运行时从环境变量读取"
|
|
727
|
+
>
|
|
728
|
+
<Input.Password placeholder="可选,建议通过系统配置管理"/>
|
|
729
|
+
</Form.Item>
|
|
730
|
+
</Form>
|
|
731
|
+
</Modal>
|
|
732
|
+
</div>
|
|
733
|
+
)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export default ModelManage
|