@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,1379 @@
1
+ import {useEffect, useRef, useState} from 'react'
2
+ import {
3
+ Alert,
4
+ Avatar,
5
+ Button,
6
+ Card,
7
+ Card as AntCard,
8
+ Col,
9
+ Divider,
10
+ Drawer,
11
+ Empty,
12
+ Form,
13
+ Input,
14
+ message,
15
+ Modal,
16
+ Popconfirm,
17
+ QRCode,
18
+ Row,
19
+ Select,
20
+ Space,
21
+ Switch,
22
+ Table,
23
+ Tabs,
24
+ Tag,
25
+ Tooltip,
26
+ Tree,
27
+ TreeSelect,
28
+ Typography
29
+ } from 'antd'
30
+ import {
31
+ AppstoreFilled,
32
+ BookOutlined,
33
+ CheckCircleFilled,
34
+ CopyOutlined,
35
+ DeleteOutlined,
36
+ DiffOutlined,
37
+ EditOutlined,
38
+ EyeOutlined,
39
+ FolderOpenOutlined,
40
+ FolderOutlined,
41
+ GlobalOutlined,
42
+ LinkOutlined,
43
+ LoadingOutlined,
44
+ LockOutlined,
45
+ PlusOutlined,
46
+ ReloadOutlined,
47
+ RobotOutlined,
48
+ RocketOutlined,
49
+ SettingOutlined,
50
+ ShareAltOutlined,
51
+ StarOutlined,
52
+ UnorderedListOutlined
53
+ } from '@ant-design/icons'
54
+ import {agentApi, llmApi} from '../../api'
55
+ import AgentAppEditor from './editor/AgentAppEditor'
56
+
57
+ const {TextArea} = Input
58
+ const {Option} = Select
59
+ const {TabPane} = Tabs
60
+ const {Text, Title} = Typography
61
+
62
+ // ===== 应用列表页 =====
63
+ const AgentAppPage = () => {
64
+ const [selectedGroupCode, setSelectedGroupCode] = useState('')
65
+ const [selectedGroupName, setSelectedGroupName] = useState('')
66
+ const [refreshKey, setRefreshKey] = useState(0)
67
+ const [editingApp, setEditingApp] = useState(null)
68
+ const handleGroupRefresh = () => setRefreshKey(k => k + 1)
69
+
70
+ if (editingApp) {
71
+ return (
72
+ <div style={{
73
+ flex: 1,
74
+ overflow: 'hidden',
75
+ borderRadius: 8,
76
+ background: '#fff',
77
+ boxShadow: '0 1px 4px rgba(0,0,0,0.06)'
78
+ }}>
79
+ <AgentAppEditor
80
+ app={editingApp}
81
+ onBack={() => {
82
+ setEditingApp(null);
83
+ handleGroupRefresh()
84
+ }}
85
+ onSaved={() => {
86
+ setEditingApp(null);
87
+ handleGroupRefresh()
88
+ }}
89
+ />
90
+ </div>
91
+ )
92
+ }
93
+
94
+ return (
95
+ <div style={{display: 'flex', gap: 16, height: 'calc(100vh - 180px)', padding: 16}}>
96
+ {/* 左侧:分组树 */}
97
+ <Card size="small" style={{width: 300, overflow: 'auto', flexShrink: 0}}
98
+ title="分组"
99
+ extra={
100
+ <Button size="small" icon={<ReloadOutlined/>} onClick={handleGroupRefresh}/>
101
+ }>
102
+ <GroupTree key={refreshKey}
103
+ selectedGroupCode={selectedGroupCode}
104
+ onGroupSelect={(code, name) => {
105
+ setSelectedGroupCode(code || '');
106
+ setSelectedGroupName(code ? (name || '') : '')
107
+ }}
108
+ onGroupRefresh={handleGroupRefresh}
109
+ />
110
+ </Card>
111
+
112
+ {/* 右侧:应用列表 */}
113
+ <Card size="small" style={{flex: 1, overflow: 'auto'}}
114
+ title={selectedGroupName || undefined}>
115
+ <AppListPage selectedGroupCode={selectedGroupCode} onEditApp={setEditingApp}/>
116
+ </Card>
117
+ </div>
118
+ )
119
+ }
120
+
121
+ // 模型数据(按供应商)
122
+ const MODELS_BY_PROVIDER = {
123
+ ollama: [
124
+ {code: 'qwen2.5:7b-instruct-q4_K_M', name: 'Qwen 2.5 7B'},
125
+ {code: 'qwen3:8b', name: 'Qwen3 8B'},
126
+ {code: 'llama3:latest', name: 'Llama 3'},
127
+ {code: 'qwen2.5:14b-instruct-q4_K_M', name: 'Qwen 2.5 14B'},
128
+ ],
129
+ openai: [
130
+ {code: 'gpt-4o', name: 'GPT-4o'},
131
+ {code: 'gpt-4o-mini', name: 'GPT-4o Mini'},
132
+ {code: 'gpt-4-turbo', name: 'GPT-4 Turbo'},
133
+ ],
134
+ dashscope: [
135
+ {code: 'qwen-max', name: 'Qwen Max'},
136
+ {code: 'qwen-plus', name: 'Qwen Plus'},
137
+ {code: 'qwen-turbo', name: 'Qwen Turbo'},
138
+ ],
139
+ }
140
+
141
+ // ===== 分组树辅助函数 =====
142
+ const buildGroupTree = (groups) => {
143
+ const itemMap = {}
144
+ groups.forEach(g => {
145
+ itemMap[g.groupCode] = {
146
+ key: g.groupCode,
147
+ title: g.groupName,
148
+ children: [],
149
+ }
150
+ })
151
+ const roots = []
152
+ groups.forEach(g => {
153
+ const node = itemMap[g.groupCode]
154
+ if (g.parentCode && itemMap[g.parentCode]) {
155
+ itemMap[g.parentCode].children.push(node)
156
+ } else if (!g.parentCode) {
157
+ roots.push(node)
158
+ }
159
+ })
160
+ return roots
161
+ }
162
+
163
+ // ===== 分组树 =====
164
+ const GroupTree = ({selectedGroupCode, onGroupSelect, onGroupRefresh}) => {
165
+ const [groups, setGroups] = useState([])
166
+ const [loading, setLoading] = useState(false)
167
+ const [modalVisible, setModalVisible] = useState(false)
168
+ const [editingGroup, setEditingGroup] = useState(null)
169
+ const [parentForCreate, setParentForCreate] = useState(null)
170
+ const [form] = Form.useForm()
171
+ const inputRef = useRef(null)
172
+
173
+ // 点击处理:选中/取消选中
174
+ const handleNodeClick = (groupCode) => {
175
+ // 点击已选中节点 → 取消选中
176
+ onGroupSelect(groupCode === selectedGroupCode ? '' : groupCode, '')
177
+ }
178
+
179
+ const loadGroups = () => {
180
+ setLoading(true)
181
+ agentApi.groupTree().then(res => {
182
+ setGroups(res?.data || res || [])
183
+ }).finally(() => setLoading(false))
184
+ }
185
+
186
+ useEffect(() => {
187
+ loadGroups()
188
+ }, [])
189
+
190
+ // 构造嵌套树数据
191
+ const buildTreeData = () => {
192
+ const itemMap = {}
193
+ groups.forEach(g => {
194
+ itemMap[g.groupCode] = {
195
+ key: g.groupCode,
196
+ title: g.groupName,
197
+ groupCode: g.groupCode,
198
+ parentCode: g.parentCode || '',
199
+ icon: selectedGroupCode === g.groupCode ? <FolderOpenOutlined/> : <FolderOutlined/>,
200
+ data: g,
201
+ children: [],
202
+ }
203
+ })
204
+ const roots = []
205
+ groups.forEach(g => {
206
+ const node = itemMap[g.groupCode]
207
+ if (g.parentCode && itemMap[g.parentCode]) {
208
+ itemMap[g.parentCode].children.push(node)
209
+ } else if (!g.parentCode) {
210
+ roots.push(node)
211
+ }
212
+ })
213
+ return roots
214
+ }
215
+
216
+ // 节点标题渲染
217
+ const titleRender = (node) => {
218
+ const data = groups.find(g => g.groupCode === node.groupCode)
219
+ return (
220
+ <span
221
+ style={{display: 'flex', alignItems: 'center', gap: 4, padding: '2px 0', cursor: 'pointer'}}
222
+ onClick={() => handleNodeClick(node.groupCode)}
223
+ >
224
+ <span style={{flex: 1, fontSize: 13}}>{node.title}</span>
225
+ <>
226
+ <Button type="text" size="small"
227
+ icon={<PlusOutlined style={{fontSize: 11, color: '#1890ff'}}/>}
228
+ onClick={(e) => {
229
+ e.stopPropagation();
230
+ setParentForCreate(data);
231
+ setEditingGroup(null);
232
+ form.resetFields();
233
+ form.setFieldsValue({parentCode: node.groupCode});
234
+ setModalVisible(true)
235
+ }}
236
+ title="新增子分组"/>
237
+ <Button type="text" size="small"
238
+ icon={<EditOutlined style={{fontSize: 11}}/>}
239
+ onClick={(e) => {
240
+ e.stopPropagation();
241
+ setEditingGroup(data);
242
+ form.setFieldsValue({groupName: data.groupName, parentCode: data.parentCode || ''});
243
+ setParentForCreate(null);
244
+ setModalVisible(true)
245
+ }}/>
246
+ <Popconfirm title={`删除分组"${data.groupName}"?`} description="子分组也会被删除"
247
+ onConfirm={() => handleDelete(node.groupCode)} onCancel={(e) => e?.stopPropagation()}>
248
+ <Button type="text" size="small" danger
249
+ icon={<DeleteOutlined style={{fontSize: 11}}/>}
250
+ onClick={(e) => e.stopPropagation()}/>
251
+ </Popconfirm>
252
+ </>
253
+ </span>
254
+ )
255
+ }
256
+
257
+ const handleDelete = async (groupCode) => {
258
+ try {
259
+ await agentApi.groupDelete(groupCode)
260
+ message.success('删除成功')
261
+ onGroupRefresh()
262
+ } catch (e) {
263
+ message.error('删除失败')
264
+ }
265
+ }
266
+
267
+ const handleSubmit = async () => {
268
+ try {
269
+ const values = await form.validateFields()
270
+ setLoading(true)
271
+ if (editingGroup) {
272
+ await agentApi.groupUpdate({
273
+ groupCode: editingGroup.groupCode,
274
+ groupName: values.groupName,
275
+ parentCode: values.parentCode || ''
276
+ })
277
+ message.success('更新成功')
278
+ } else {
279
+ await agentApi.groupCreate({
280
+ groupName: values.groupName,
281
+ parentCode: values.parentCode || '',
282
+ groupCode: 'grp_' + Date.now()
283
+ })
284
+ message.success('创建成功')
285
+ }
286
+ setModalVisible(false)
287
+ onGroupRefresh()
288
+ } catch (e) {
289
+ message.error('操作失败')
290
+ } finally {
291
+ setLoading(false)
292
+ }
293
+ }
294
+
295
+ return (
296
+ <div style={{padding: '4px 0'}}>
297
+ {/* 树节点 */}
298
+ <div style={{maxHeight: 'calc(100vh - 320px)', overflow: 'auto'}}>
299
+ <Tree
300
+ treeData={buildTreeData()}
301
+ defaultExpandAll
302
+ showLine
303
+ blockNode
304
+ titleRender={titleRender}
305
+ selectedKeys={selectedGroupCode ? [selectedGroupCode] : []}
306
+ onSelect={() => {
307
+ }}
308
+ />
309
+ </div>
310
+
311
+ {/* 新建/编辑分组弹窗 */}
312
+ <Modal
313
+ title={editingGroup ? '编辑分组' : parentForCreate ? `新建子分组 - ${parentForCreate.groupName}` : '新建分组'}
314
+ open={modalVisible}
315
+ onCancel={() => setModalVisible(false)}
316
+ onOk={() => form.submit()}
317
+ destroyOnClose
318
+ >
319
+ <Form form={form} layout="vertical" onFinish={handleSubmit}>
320
+ <Form.Item name="groupName" label="分组名称" rules={[{required: true, message: '请输入分组名称'}]}>
321
+ <Input ref={inputRef} placeholder="如:客服机器人"/>
322
+ </Form.Item>
323
+ <Form.Item name="parentCode" label="父分组">
324
+ <Select allowClear placeholder="留空为顶级分组">
325
+ {groups.filter(g => editingGroup ? g.groupCode !== editingGroup.groupCode : true).map(g => (
326
+ <Select.Option key={g.groupCode} value={g.groupCode}>{g.groupName}</Select.Option>
327
+ ))}
328
+ </Select>
329
+ </Form.Item>
330
+ </Form>
331
+ </Modal>
332
+ </div>
333
+ )
334
+ }
335
+
336
+ // ===== 应用卡片 =====
337
+ const AppCard = ({
338
+ record,
339
+ onEdit,
340
+ onView,
341
+ onPublish,
342
+ onUpgrade,
343
+ onShare,
344
+ onDelete,
345
+ onToolConfig,
346
+ onToggleShare,
347
+ loading
348
+ }) => {
349
+ const isPublished = record.status === 'PUBLISHED'
350
+ const version = record.version || 1
351
+ const shareEnabled = !!record.shareEnabled
352
+
353
+ return (
354
+ <AntCard
355
+ hoverable
356
+ style={{
357
+ borderRadius: 12,
358
+ border: '1px solid #f0f0f0',
359
+ transition: 'all 0.3s',
360
+ }}
361
+ bodyStyle={{padding: 16}}
362
+ actions={[
363
+ isPublished ? (
364
+ <Tooltip key="view" title="查看发布版本(只读)">
365
+ <Button type="text" icon={<EyeOutlined/>} onClick={() => onView?.(record)}>查看</Button>
366
+ </Tooltip>
367
+ ) : (
368
+ <Tooltip key="edit" title="编辑草稿">
369
+ <Button type="text" icon={<EditOutlined/>} onClick={() => onEdit(record)}>编辑</Button>
370
+ </Tooltip>
371
+ ),
372
+ isPublished ? (
373
+ <Tooltip key="upgrade" title="基于当前版本创建新草稿">
374
+ <Button type="text" icon={<DiffOutlined/>} onClick={() => onUpgrade?.(record)}>升级</Button>
375
+ </Tooltip>
376
+ ) : (
377
+ <Tooltip key="publish" title="发布后即可分享">
378
+ <Button type="text" icon={<RocketOutlined/>}
379
+ onClick={() => onPublish(record.appCode)}>发布</Button>
380
+ </Tooltip>
381
+ ),
382
+ <Tooltip key="share" title={shareEnabled ? '查看/复制分享链接' : '开启后生成分享链接'}>
383
+ <Button type="text"
384
+ icon={shareEnabled ? <GlobalOutlined style={{color: '#52c41a'}}/> : <LockOutlined/>}
385
+ onClick={() => onShare?.(record)}>分享</Button>
386
+ </Tooltip>,
387
+ <Tooltip key="tools" title="工具配置">
388
+ <Button type="text" icon={<SettingOutlined/>} onClick={() => onToolConfig?.(record)}>工具</Button>
389
+ </Tooltip>,
390
+ <Popconfirm key="delete" title="确定删除此应用?" onConfirm={() => onDelete(record.id)} okText="删除"
391
+ okButtonProps={{danger: true}}>
392
+ <Button type="text" danger icon={<DeleteOutlined/>}>删除</Button>
393
+ </Popconfirm>,
394
+ ]}
395
+ >
396
+ <div style={{display: 'flex', gap: 16}}>
397
+ <Avatar
398
+ shape="square"
399
+ size={64}
400
+ src={record.iconUrl}
401
+ icon={<RobotOutlined/>}
402
+ style={{
403
+ background: isPublished
404
+ ? 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)'
405
+ : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
406
+ flexShrink: 0
407
+ }}
408
+ />
409
+ <div style={{flex: 1, minWidth: 0}}>
410
+ <div style={{
411
+ display: 'flex',
412
+ alignItems: 'flex-start',
413
+ justifyContent: 'space-between',
414
+ marginBottom: 8
415
+ }}>
416
+ <div>
417
+ <Title level={5} style={{margin: 0, color: '#262626'}}>{record.appName}</Title>
418
+ <Text type="secondary"
419
+ style={{fontSize: 12, fontFamily: 'monospace'}}>{record.appCode}</Text>
420
+ </div>
421
+ <Space size={4}>
422
+ <Tag style={{borderRadius: 20, fontSize: 11}}>v{version}</Tag>
423
+ <Tag color={isPublished ? 'success' : 'default'} style={{borderRadius: 20}}>
424
+ {isPublished ? '已发布' : '草稿'}
425
+ </Tag>
426
+ {shareEnabled && (
427
+ <Tooltip title="已开启分享,点击「分享」按钮获取链接">
428
+ <Tag color="green" icon={<GlobalOutlined/>}
429
+ style={{borderRadius: 20, fontSize: 11, margin: 0}}>
430
+ 已分享
431
+ </Tag>
432
+ </Tooltip>
433
+ )}
434
+ </Space>
435
+ </div>
436
+
437
+ {record.description && (
438
+ <div style={{color: '#595959', fontSize: 13, marginBottom: 12, lineHeight: 1.6}}>
439
+ {record.description}
440
+ </div>
441
+ )}
442
+
443
+ {/* 模型信息 */}
444
+ <div style={{display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12}}>
445
+ <Tag color="blue" style={{margin: 0}}>{record.modelProvider || 'ollama'}</Tag>
446
+ <Text style={{fontSize: 13, color: '#8c8c8c'}}>{record.modelName || 'qwen2.5:7b'}</Text>
447
+ </div>
448
+
449
+ {/* 时间信息 */}
450
+ <div style={{display: 'flex', gap: 16, fontSize: 12, color: '#8c8c8c', marginTop: 8}}>
451
+ <span>创建于 {record.gmtCreate?.split(' ')[0] || '-'}</span>
452
+ {record.gmtModified !== record.gmtCreate && (
453
+ <span>更新于 {record.gmtModified?.split(' ')[0] || '-'}</span>
454
+ )}
455
+ </div>
456
+ </div>
457
+ </div>
458
+ </AntCard>
459
+ )
460
+ }
461
+
462
+ // ===== 分享抽屉 · 未开启状态 =====
463
+ const ShareDrawerEmpty = ({app, onEnable}) => {
464
+ const [loading, setLoading] = useState(false)
465
+ return (
466
+ <div style={{padding: '24px 0', textAlign: 'center'}}>
467
+ <div style={{
468
+ width: 96, height: 96, borderRadius: '50%', margin: '24px auto',
469
+ background: 'linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%)',
470
+ display: 'flex', alignItems: 'center', justifyContent: 'center'
471
+ }}>
472
+ <LockOutlined style={{fontSize: 40, color: '#52c41a'}}/>
473
+ </div>
474
+ <Title level={5} style={{marginTop: 8}}>该应用尚未开启分享</Title>
475
+ <Text type="secondary" style={{display: 'block', marginBottom: 24, fontSize: 13}}>
476
+ 开启后,任何人都可通过专属链接访问该 Agent<br/>
477
+ 无需登录、可嵌入网页 / 群聊 / 二维码
478
+ </Text>
479
+ <Button
480
+ type="primary"
481
+ size="large"
482
+ icon={<GlobalOutlined/>}
483
+ loading={loading}
484
+ onClick={async () => {
485
+ setLoading(true);
486
+ try {
487
+ await onEnable()
488
+ } finally {
489
+ setLoading(false)
490
+ }
491
+ }}
492
+ style={{borderRadius: 8, minWidth: 160}}
493
+ >
494
+ 开启分享
495
+ </Button>
496
+ <Divider style={{margin: '32px 0 16px'}} plain>
497
+ <Text type="secondary" style={{fontSize: 12}}>分享后你可以获得</Text>
498
+ </Divider>
499
+ <Space orientation="vertical" size="small" style={{width: '100%', textAlign: 'left', padding: '0 16px'}}>
500
+ {[
501
+ {icon: <LinkOutlined/>, text: '独立的访问链接与二维码'},
502
+ {icon: <CopyOutlined/>, text: '一键复制到微信 / 飞书 / 邮件'},
503
+ {icon: <EyeOutlined/>, text: '实时统计访问人数与对话轮次'},
504
+ ].map((item, i) => (
505
+ <div key={i}
506
+ style={{display: 'flex', alignItems: 'center', gap: 8, color: '#595959', fontSize: 13}}>
507
+ <span style={{color: '#52c41a'}}>{item.icon}</span>
508
+ {item.text}
509
+ </div>
510
+ ))}
511
+ </Space>
512
+ </div>
513
+ )
514
+ }
515
+
516
+ // ===== 分享抽屉 · 已开启状态 =====
517
+ const ShareDrawerContent = ({app, onDisable, onRefresh}) => {
518
+ const shareLink = app.shareCode
519
+ ? `${window.location.origin}/share/${app.shareCode}`
520
+ : ''
521
+ const [copied, setCopied] = useState(false)
522
+ const [refreshing, setRefreshing] = useState(false)
523
+
524
+ const copy = async () => {
525
+ if (!shareLink) return
526
+ try {
527
+ await navigator.clipboard.writeText(shareLink)
528
+ setCopied(true)
529
+ message.success('链接已复制')
530
+ setTimeout(() => setCopied(false), 2000)
531
+ } catch {
532
+ const ta = document.createElement('textarea')
533
+ ta.value = shareLink;
534
+ document.body.appendChild(ta);
535
+ ta.select()
536
+ document.execCommand('copy');
537
+ document.body.removeChild(ta)
538
+ setCopied(true);
539
+ message.success('链接已复制')
540
+ setTimeout(() => setCopied(false), 2000)
541
+ }
542
+ }
543
+
544
+ const handleRefresh = async () => {
545
+ setRefreshing(true)
546
+ try {
547
+ await onRefresh()
548
+ } finally {
549
+ setRefreshing(false)
550
+ }
551
+ }
552
+
553
+ return (
554
+ <div>
555
+ {/* 顶部状态条 */}
556
+ <Alert
557
+ type="success"
558
+ showIcon
559
+ message="已开启分享"
560
+ description="链接现在可被任何人访问,关闭分享后旧链接立即失效。"
561
+ style={{borderRadius: 8, marginBottom: 20}}
562
+ />
563
+
564
+ {/* 二维码 + 链接 */}
565
+ <div style={{
566
+ display: 'flex', gap: 20, padding: 20, background: '#fafafa',
567
+ borderRadius: 12, alignItems: 'center', marginBottom: 20
568
+ }}>
569
+ <div style={{
570
+ background: '#fff', padding: 12, borderRadius: 8,
571
+ boxShadow: '0 1px 3px rgba(0,0,0,0.06)', flexShrink: 0
572
+ }}>
573
+ {shareLink ? (
574
+ <QRCode value={shareLink} size={140} bordered={false}/>
575
+ ) : (
576
+ <div style={{
577
+ width: 140,
578
+ height: 140,
579
+ display: 'flex',
580
+ alignItems: 'center',
581
+ justifyContent: 'center'
582
+ }}>
583
+ <Text type="secondary">无链接</Text>
584
+ </div>
585
+ )}
586
+ </div>
587
+ <div style={{flex: 1, minWidth: 0}}>
588
+ <Text type="secondary" style={{fontSize: 12}}>分享链接</Text>
589
+ <Input.TextArea
590
+ value={shareLink}
591
+ readOnly
592
+ autoSize={{minRows: 2, maxRows: 3}}
593
+ style={{marginTop: 4, fontSize: 12, fontFamily: 'monospace', resize: 'none'}}
594
+ onClick={(e) => e.target.select()}
595
+ />
596
+ <Space style={{marginTop: 8}} size="small">
597
+ <Button
598
+ type="primary"
599
+ size="small"
600
+ icon={copied ? <CheckCircleFilled/> : <CopyOutlined/>}
601
+ onClick={copy}
602
+ disabled={!shareLink}
603
+ >
604
+ {copied ? '已复制' : '复制链接'}
605
+ </Button>
606
+ <Button
607
+ size="small"
608
+ icon={<LinkOutlined/>}
609
+ onClick={() => shareLink && window.open(shareLink, '_blank')}
610
+ disabled={!shareLink}
611
+ >
612
+ 打开
613
+ </Button>
614
+ <Button
615
+ size="small"
616
+ icon={<ReloadOutlined/>}
617
+ loading={refreshing}
618
+ onClick={handleRefresh}
619
+ >
620
+ 刷新链接
621
+ </Button>
622
+ </Space>
623
+ </div>
624
+ </div>
625
+
626
+ {/* 设置项 */}
627
+ <div style={{marginBottom: 20}}>
628
+ <Title level={5} style={{fontSize: 13, marginBottom: 12}}>分享设置</Title>
629
+ <div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12}}>
630
+ <div style={{padding: 12, background: '#fafafa', borderRadius: 8}}>
631
+ <Text type="secondary" style={{fontSize: 11}}>访问方式</Text>
632
+ <div style={{marginTop: 4}}>
633
+ <Tag color="blue" style={{margin: 0}}>公开链接</Tag>
634
+ </div>
635
+ </div>
636
+ <div style={{padding: 12, background: '#fafafa', borderRadius: 8}}>
637
+ <Text type="secondary" style={{fontSize: 11}}>是否需要登录</Text>
638
+ <div style={{marginTop: 4}}>
639
+ <Tag style={{margin: 0}}>不需要</Tag>
640
+ </div>
641
+ </div>
642
+ <div style={{padding: 12, background: '#fafafa', borderRadius: 8}}>
643
+ <Text type="secondary" style={{fontSize: 11}}>创建时间</Text>
644
+ <div style={{marginTop: 4, fontSize: 13}}>
645
+ {app.shareGmtCreate?.split(' ')[0] || app.gmtModified?.split(' ')[0] || '-'}
646
+ </div>
647
+ </div>
648
+ <div style={{padding: 12, background: '#fafafa', borderRadius: 8}}>
649
+ <Text type="secondary" style={{fontSize: 11}}>累计访问</Text>
650
+ <div style={{marginTop: 4, fontSize: 13}}>
651
+ {app.shareVisitCount ?? 0} 次
652
+ </div>
653
+ </div>
654
+ </div>
655
+ </div>
656
+
657
+ {/* 危险操作 */}
658
+ <Divider style={{margin: '12px 0'}}/>
659
+ <Popconfirm
660
+ title="确定要关闭分享吗?"
661
+ description="关闭后,链接将立即失效。"
662
+ okText="关闭分享"
663
+ okButtonProps={{danger: true}}
664
+ cancelText="取消"
665
+ onConfirm={onDisable}
666
+ >
667
+ <Button block danger icon={<LockOutlined/>}>
668
+ 关闭分享
669
+ </Button>
670
+ </Popconfirm>
671
+ </div>
672
+ )
673
+ }
674
+
675
+ // ===== 应用列表(左侧分组树 + 右侧卡片) =====
676
+ const AppListPage = ({selectedGroupCode, onEditApp}) => {
677
+ const [dataSource, setDataSource] = useState([])
678
+ const [loading, setLoading] = useState(false)
679
+ const [searchKey, setSearchKey] = useState('')
680
+ const [statusFilter, setStatusFilter] = useState('')
681
+ const [pageNum, setPageNum] = useState(1)
682
+ const [pageSize] = useState(12)
683
+ const [total, setTotal] = useState(0)
684
+ const [createLoading, setCreateLoading] = useState(false)
685
+ const [groups, setGroups] = useState([])
686
+ const [toolDrawerVisible, setToolDrawerVisible] = useState(false)
687
+ const [toolDrawerApp, setToolDrawerApp] = useState(null)
688
+ const [shareDrawerVisible, setShareDrawerVisible] = useState(false)
689
+ const [shareDrawerApp, setShareDrawerApp] = useState(null)
690
+ // 视图模式
691
+ const [viewMode, setViewMode] = useState('card')
692
+ // 创建弹窗的模型列表
693
+ const [createModelOptions, setCreateModelOptions] = useState(MODELS_BY_PROVIDER.ollama)
694
+
695
+ // 新建应用 Modal(简版)
696
+ const [createModalVisible, setCreateModalVisible] = useState(false)
697
+ const [createForm] = Form.useForm()
698
+ const [groupOptions, setGroupOptions] = useState([])
699
+
700
+ useEffect(() => {
701
+ fetchData();
702
+ handleGroupRefresh()
703
+ }, [pageNum, searchKey, statusFilter, selectedGroupCode])
704
+
705
+ const fetchData = () => {
706
+ setLoading(true)
707
+ agentApi.appPage({
708
+ appName: searchKey,
709
+ status: statusFilter,
710
+ groupCode: selectedGroupCode,
711
+ id: pageNum
712
+ }).then(pageData => {
713
+ if (pageData) {
714
+ setDataSource(pageData.records || [])
715
+ setTotal(pageData.total || 0)
716
+ }
717
+ }).catch(e => message.error('加载失败')).finally(() => setLoading(false))
718
+ }
719
+
720
+ const handleGroupRefresh = () => {
721
+ agentApi.groupTree().then(res => {
722
+ const list = res?.data || res || []
723
+ setGroups(list)
724
+ setGroupOptions(list.filter(g => g.groupCode))
725
+ fetchData()
726
+ })
727
+ }
728
+
729
+ // ===== 新建(简版) =====
730
+ const handleCreate = () => {
731
+ createForm.resetFields()
732
+ const provider = 'ollama'
733
+ const models = MODELS_BY_PROVIDER[provider] || []
734
+ setCreateModelOptions(models)
735
+ createForm.setFieldsValue({
736
+ modelProvider: provider,
737
+ modelName: models[0]?.code || '',
738
+ groupCode: selectedGroupCode
739
+ })
740
+ setCreateModalVisible(true)
741
+ }
742
+
743
+ const handleCreateSubmit = async () => {
744
+ try {
745
+ const values = await createForm.validateFields()
746
+ setCreateLoading(true)
747
+ await agentApi.appCreate(values)
748
+ message.success('创建成功')
749
+ setCreateModalVisible(false)
750
+ fetchData()
751
+ } catch (e) {
752
+ if (!e.errorFields) message.error('操作失败')
753
+ } finally {
754
+ setCreateLoading(false)
755
+ }
756
+ }
757
+
758
+ // ===== 编辑 =====
759
+ const handleEdit = (record) => {
760
+ onEditApp(record)
761
+ }
762
+
763
+ // ===== 查看(发布版本只读) =====
764
+ // 编辑器内部已根据 record.status 自动设置 disabled,这里仅作语义区分
765
+ const handleView = (record) => {
766
+ onEditApp(record)
767
+ }
768
+
769
+ const handleDelete = async (id) => {
770
+ try {
771
+ await agentApi.appDelete(id);
772
+ message.success('删除成功');
773
+ fetchData()
774
+ } catch (e) {
775
+ message.error('删除失败')
776
+ }
777
+ }
778
+
779
+ const handlePublish = async (appCode) => {
780
+ try {
781
+ await agentApi.appPublish(appCode);
782
+ message.success('发布成功');
783
+ fetchData()
784
+ } catch (e) {
785
+ message.error('发布失败')
786
+ }
787
+ }
788
+
789
+ const handleUpgrade = async (record) => {
790
+ try {
791
+ await agentApi.appUpgrade(record.appCode)
792
+ message.success('已升级到新草稿版本,可编辑后发布')
793
+ fetchData()
794
+ // 重新拉取最新 app(含 status=DRAFT 和新 version)并跳到编辑器
795
+ const updated = await agentApi.appGet(record.appCode)
796
+ const appData = updated?.data || updated
797
+ if (appData) onEditApp(appData)
798
+ } catch (e) {
799
+ message.error('升级失败:' + (e?.message || '未知错误'))
800
+ }
801
+ }
802
+
803
+ const handleShare = (record) => {
804
+ // 总是打开右侧抽屉,由抽屉内部处理开启/关闭分享
805
+ setShareDrawerApp(record)
806
+ setShareDrawerVisible(true)
807
+ }
808
+
809
+ const handleToggleShare = async (record, enabled) => {
810
+ try {
811
+ const res = await agentApi.appToggleShare(record.appCode, enabled)
812
+ const code = res?.data || res
813
+ message.success(enabled ? '已开启分享' : '已关闭分享')
814
+ // 本地更新 record 避免再次拉取
815
+ setDataSource(prev => prev.map(r => r.id === record.id ? {
816
+ ...r,
817
+ shareEnabled: enabled,
818
+ shareCode: code || r.shareCode
819
+ } : r))
820
+ // 同步更新抽屉中的 app
821
+ setShareDrawerApp(prev => prev && prev.id === record.id ? {
822
+ ...prev,
823
+ shareEnabled: enabled,
824
+ shareCode: code || prev.shareCode
825
+ } : prev)
826
+ } catch (e) {
827
+ message.error('操作失败:' + (e?.message || '未知错误'))
828
+ }
829
+ }
830
+
831
+ const copyShareLink = (shareCode) => {
832
+ const link = `${window.location.origin}/share/${shareCode}`
833
+ navigator.clipboard.writeText(link).then(() => message.success('链接已复制')).catch(() => {
834
+ const ta = document.createElement('textarea');
835
+ ta.value = link;
836
+ document.body.appendChild(ta);
837
+ ta.select();
838
+ document.execCommand('copy');
839
+ document.body.removeChild(ta);
840
+ message.success('链接已复制')
841
+ })
842
+ }
843
+
844
+ const publishedCount = dataSource.filter(r => r.status === 'PUBLISHED').length
845
+
846
+ return (
847
+ <div>
848
+ {/* 搜索和操作栏 */}
849
+ <div style={{
850
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
851
+ marginBottom: 16, padding: '16px 20px', background: '#fafafa', borderRadius: 8
852
+ }}>
853
+ <Space wrap>
854
+ <Input.Search placeholder="搜索应用名称/编码" onSearch={v => {
855
+ setSearchKey(v);
856
+ setPageNum(1)
857
+ }} style={{width: 280}} allowClear enterButton/>
858
+ <Select placeholder="状态筛选" allowClear style={{width: 140}} onChange={v => {
859
+ setStatusFilter(v);
860
+ setPageNum(1)
861
+ }}>
862
+ <Option value="DRAFT">草稿</Option>
863
+ <Option value="PUBLISHED">已发布</Option>
864
+ </Select>
865
+ </Space>
866
+ <Space>
867
+ {total > 0 && (<Text type="secondary">共 <Text strong>{total}</Text> 个应用,已发布 <Text strong
868
+ style={{color: '#52c41a'}}>{publishedCount}</Text> 个</Text>)}
869
+ <Button size="small" type={viewMode === 'card' ? 'primary' : 'default'} icon={<AppstoreFilled/>}
870
+ onClick={() => setViewMode('card')} style={{borderRadius: 6}}/>
871
+ <Button size="small" type={viewMode === 'list' ? 'primary' : 'default'}
872
+ icon={<UnorderedListOutlined/>} onClick={() => setViewMode('list')}
873
+ style={{borderRadius: 6}}/>
874
+ <Button type="primary" icon={<PlusOutlined/>} onClick={handleCreate}
875
+ style={{borderRadius: 8}}>新建应用</Button>
876
+ </Space>
877
+ </div>
878
+
879
+ {/* 加载状态 */}
880
+ {loading && (<div style={{textAlign: 'center', padding: 48}}><LoadingOutlined
881
+ style={{fontSize: 32, color: '#667eea'}}/>
882
+ <div style={{marginTop: 16, color: '#8c8c8c'}}>加载中...</div>
883
+ </div>)}
884
+
885
+ {/* 空状态 */}
886
+ {!loading && dataSource.length === 0 && (
887
+ <div style={{textAlign: 'center', padding: '64px 24px', background: '#fafafa', borderRadius: 12}}>
888
+ <div style={{
889
+ width: 80,
890
+ height: 80,
891
+ borderRadius: '50%',
892
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
893
+ display: 'flex',
894
+ alignItems: 'center',
895
+ justifyContent: 'center',
896
+ margin: '0 auto 24px'
897
+ }}>
898
+ <RobotOutlined style={{fontSize: 36, color: '#fff'}}/>
899
+ </div>
900
+ <Title level={4} style={{marginBottom: 8}}>还没有应用</Title>
901
+ <Text type="secondary" style={{display: 'block', marginBottom: 24}}>点击「新建应用」开始创建您的第一个
902
+ Agent</Text>
903
+ <Button type="primary" icon={<PlusOutlined/>} onClick={handleCreate}
904
+ style={{borderRadius: 8}}>新建应用</Button>
905
+ </div>
906
+ )}
907
+
908
+ {/* 应用卡片/列表 */}
909
+ {!loading && dataSource.length > 0 && viewMode === 'card' && (
910
+ <div style={{
911
+ display: 'grid',
912
+ gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))',
913
+ gap: 16,
914
+ marginBottom: 16
915
+ }}>
916
+ {dataSource.map(record => (
917
+ <AppCard key={record.id} record={record} onEdit={handleEdit} onView={handleView}
918
+ onPublish={handlePublish} onUpgrade={handleUpgrade} onShare={handleShare}
919
+ onDelete={handleDelete} onToolConfig={(app) => {
920
+ setToolDrawerApp(app);
921
+ setToolDrawerVisible(true)
922
+ }} onShareManage={(app) => {
923
+ setShareDrawerApp(app);
924
+ setShareDrawerVisible(true)
925
+ }}/>
926
+ ))}
927
+ </div>
928
+ )}
929
+
930
+ {!loading && dataSource.length > 0 && viewMode === 'list' && (
931
+ <Table
932
+ dataSource={dataSource}
933
+ rowKey="id"
934
+ size="small"
935
+ pagination={false}
936
+ columns={[
937
+ {
938
+ title: '名称',
939
+ dataIndex: 'appName',
940
+ width: 180,
941
+ render: (t, r) => <a onClick={() => handleEdit(r)}
942
+ style={{fontWeight: 500}}>{t || r.appCode}</a>
943
+ },
944
+ {
945
+ title: '版本',
946
+ dataIndex: 'version',
947
+ width: 60,
948
+ render: v => <Tag style={{fontSize: 11}}>v{v || 1}</Tag>
949
+ },
950
+ {
951
+ title: '编码',
952
+ dataIndex: 'appCode',
953
+ width: 180,
954
+ render: t => <code style={{fontSize: 12, color: '#8c8c8c'}}>{t}</code>
955
+ },
956
+ {
957
+ title: '状态',
958
+ dataIndex: 'status',
959
+ width: 80,
960
+ render: s => <Tag
961
+ color={s === 'PUBLISHED' ? 'green' : 'default'}>{s === 'PUBLISHED' ? '已发布' : '草稿'}</Tag>
962
+ },
963
+ {title: '供应商', dataIndex: 'modelProvider', width: 100},
964
+ {title: '模型', dataIndex: 'modelName', width: 140, ellipsis: true},
965
+ {title: '创建时间', dataIndex: 'gmtCreate', width: 170},
966
+ {
967
+ title: '操作', width: 220, render: (_, r) => (
968
+ <Space size="small">
969
+ {r.status === 'PUBLISHED' ? (
970
+ <>
971
+ <Button size="small" type="link" icon={<EyeOutlined/>}
972
+ onClick={() => handleView(r)}>查看</Button>
973
+ <Button size="small" type="link" icon={<DiffOutlined/>}
974
+ onClick={() => handleUpgrade(r)}>升级</Button>
975
+ </>
976
+ ) : (
977
+ <>
978
+ <Button size="small" type="link" icon={<EditOutlined/>}
979
+ onClick={() => handleEdit(r)}>编辑</Button>
980
+ <Button size="small" type="link" icon={<RocketOutlined/>}
981
+ onClick={() => handlePublish(r.appCode)}>发布</Button>
982
+ </>
983
+ )}
984
+ <Popconfirm title="确定删除?" onConfirm={() => handleDelete(r.id)}>
985
+ <Button size="small" type="link" danger icon={<DeleteOutlined/>}>删除</Button>
986
+ </Popconfirm>
987
+ </Space>
988
+ )
989
+ },
990
+ ]}
991
+ />
992
+ )}
993
+
994
+ {/* 分页 */}
995
+ {!loading && total > pageSize && (
996
+ <div style={{textAlign: 'center', marginTop: 16}}>
997
+ <Button onClick={() => setPageNum(p => Math.max(1, p - 1))} disabled={pageNum === 1}>上一页</Button>
998
+ <span style={{margin: '0 16px'}}>第 <Text strong>{pageNum}</Text> / {Math.ceil(total / pageSize)} 页</span>
999
+ <Button onClick={() => setPageNum(p => p + 1)}
1000
+ disabled={pageNum >= Math.ceil(total / pageSize)}>下一页</Button>
1001
+ </div>
1002
+ )}
1003
+
1004
+ {/* ===== 新建应用 ===== */}
1005
+ <Modal
1006
+ title="新建应用"
1007
+ open={createModalVisible}
1008
+ onOk={handleCreateSubmit}
1009
+ onCancel={() => setCreateModalVisible(false)}
1010
+ confirmLoading={createLoading}
1011
+ width={520}
1012
+ okText="创建"
1013
+ cancelText="取消"
1014
+ >
1015
+ <Form form={createForm} layout="vertical" size="middle">
1016
+ <div style={{display: 'flex', gap: 12}}>
1017
+ <Form.Item name="appName" label="应用名称" rules={[{required: true, message: '请输入应用名称'}]}
1018
+ style={{flex: 1}}>
1019
+ <Input placeholder="如:智能客服助手"/>
1020
+ </Form.Item>
1021
+ <Form.Item name="groupCode" label="分组" style={{width: 160}}>
1022
+ <TreeSelect
1023
+ allowClear
1024
+ placeholder="默认"
1025
+ treeData={buildGroupTree(groupOptions)}
1026
+ treeDefaultExpandAll
1027
+ onChange={(val) => createForm.setFieldsValue({groupCode: val || ''})}
1028
+ style={{width: '100%'}}
1029
+ />
1030
+ </Form.Item>
1031
+ </div>
1032
+ <Form.Item name="description" label="应用描述">
1033
+ <TextArea rows={2} placeholder="简短描述这个应用的能力和使用场景"/>
1034
+ </Form.Item>
1035
+ <Divider style={{margin: '12px 0', fontSize: 12, color: '#8c8c8c'}}>应用类型</Divider>
1036
+ <Form.Item name="agentMode" label="类型" rules={[{required: true, message: '请选择应用类型'}]}
1037
+ initialValue="chat">
1038
+ <Select>
1039
+ <Option value="chat">对话 - 纯文本对话</Option>
1040
+ <Option value="agent">智能体 - 工具调用 + 知识库</Option>
1041
+ <Option value="workflow">工作流 - 多步骤流程编排</Option>
1042
+ </Select>
1043
+ </Form.Item>
1044
+ <Divider style={{margin: '12px 0', fontSize: 12, color: '#8c8c8c'}}>模型配置</Divider>
1045
+ <Form.Item name="modelProvider" label="提供商" rules={[{required: true}]} initialValue="ollama">
1046
+ <Select onChange={(val) => {
1047
+ const models = MODELS_BY_PROVIDER[val] || []
1048
+ setCreateModelOptions(models)
1049
+ createForm.setFieldsValue({modelName: models[0]?.code || ''})
1050
+ }}>
1051
+ <Option value="ollama">Ollama (本地)</Option>
1052
+ <Option value="openai">OpenAI</Option>
1053
+ <Option value="dashscope">阿里通义</Option>
1054
+ </Select>
1055
+ </Form.Item>
1056
+ <Form.Item name="modelName" label="模型" rules={[{required: true}]}>
1057
+ <Select>
1058
+ {createModelOptions.map(m => (
1059
+ <Option key={m.code} value={m.code}>{m.name}</Option>
1060
+ ))}
1061
+ </Select>
1062
+ </Form.Item>
1063
+ </Form>
1064
+ </Modal>
1065
+
1066
+ {/* ===== 工具配置 Drawer ===== */}
1067
+ <Drawer
1068
+ title={toolDrawerApp ? `${toolDrawerApp.appName} - 工具配置` : '工具配置'}
1069
+ width={560}
1070
+ open={toolDrawerVisible}
1071
+ onClose={() => setToolDrawerVisible(false)}
1072
+ >
1073
+ {toolDrawerApp && <ToolConfigPage appCode={toolDrawerApp.appCode}/>}
1074
+ </Drawer>
1075
+
1076
+ {/* ===== 分享管理 Drawer ===== */}
1077
+ <Drawer
1078
+ title={shareDrawerApp ? `${shareDrawerApp.appName} · 分享` : '分享'}
1079
+ width={520}
1080
+ open={shareDrawerVisible}
1081
+ onClose={() => setShareDrawerVisible(false)}
1082
+ destroyOnClose
1083
+ extra={
1084
+ shareDrawerApp && (
1085
+ <Space>
1086
+ <Text type="secondary" style={{fontSize: 12}}>分享</Text>
1087
+ <Switch
1088
+ size="small"
1089
+ checked={!!shareDrawerApp.shareEnabled}
1090
+ onChange={(checked) => handleToggleShare(shareDrawerApp, checked)}
1091
+ />
1092
+ </Space>
1093
+ )
1094
+ }
1095
+ >
1096
+ {shareDrawerApp && (
1097
+ shareDrawerApp.shareEnabled ? (
1098
+ <ShareDrawerContent
1099
+ app={shareDrawerApp}
1100
+ onDisable={() => handleToggleShare(shareDrawerApp, false)}
1101
+ onRefresh={() => handleToggleShare(shareDrawerApp, true)}
1102
+ />
1103
+ ) : (
1104
+ <ShareDrawerEmpty
1105
+ app={shareDrawerApp}
1106
+ onEnable={() => handleToggleShare(shareDrawerApp, true)}
1107
+ />
1108
+ )
1109
+ )}
1110
+ </Drawer>
1111
+ </div>
1112
+ )
1113
+ }
1114
+
1115
+ // ===== 工具配置页 =====
1116
+ const ToolConfigPage = () => {
1117
+ const [apps, setApps] = useState([])
1118
+ const [selectedApp, setSelectedApp] = useState(null)
1119
+ const [tools, setTools] = useState([])
1120
+ const [skills, setSkills] = useState([])
1121
+ const [knowledge, setKnowledge] = useState([])
1122
+ const [boundTools, setBoundTools] = useState([])
1123
+ const [boundSkills, setBoundSkills] = useState([])
1124
+ const [boundKnowledge, setBoundKnowledge] = useState([])
1125
+ const [loading, setLoading] = useState(false)
1126
+ const [saving, setSaving] = useState(false)
1127
+ const [activeTab, setActiveTab] = useState('tools')
1128
+ const [appLoading, setAppLoading] = useState(false)
1129
+
1130
+ useEffect(() => {
1131
+ setAppLoading(true)
1132
+ agentApi.appPage({id: 1, pageSize: 100}).then(pageData => {
1133
+ setApps(pageData?.records || [])
1134
+ }).finally(() => setAppLoading(false))
1135
+ }, [])
1136
+
1137
+ useEffect(() => {
1138
+ if (!selectedApp) return
1139
+ setLoading(true)
1140
+ Promise.all([
1141
+ llmApi.toolTemplateList(),
1142
+ llmApi.skillTemplateList(),
1143
+ llmApi.knowledgeList(),
1144
+ ]).then(([tRes, sRes, kRes]) => {
1145
+ setTools(tRes?.data || tRes || [])
1146
+ setSkills(sRes?.data || sRes || [])
1147
+ setKnowledge(kRes?.data || kRes || [])
1148
+ loadBoundConfigs(selectedApp.appCode)
1149
+ }).finally(() => setLoading(false))
1150
+ }, [selectedApp])
1151
+
1152
+ const loadBoundConfigs = (appCode) => {
1153
+ llmApi.appConfig(appCode).then(res => {
1154
+ const cfg = res?.data || res || {}
1155
+ setBoundTools(cfg.tools || [])
1156
+ setBoundSkills(cfg.skills || [])
1157
+ setBoundKnowledge(cfg.knowledgeBases || [])
1158
+ }).catch(() => {
1159
+ })
1160
+ }
1161
+
1162
+ const isBound = (list, code) => list.includes(code)
1163
+
1164
+ const toggleItem = (type, code) => {
1165
+ if (type === 'tool') setBoundTools(prev => prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code])
1166
+ else if (type === 'skill') setBoundSkills(prev => prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code])
1167
+ else setBoundKnowledge(prev => prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code])
1168
+ }
1169
+
1170
+ const handleSave = async () => {
1171
+ if (!selectedApp) return
1172
+ setSaving(true)
1173
+ try {
1174
+ for (const toolCode of boundTools) await llmApi.appToolAdd({
1175
+ appCode: selectedApp.appCode,
1176
+ toolCode
1177
+ }).catch(() => {
1178
+ })
1179
+ for (const skillId of boundSkills) await llmApi.appSkillAdd({
1180
+ appCode: selectedApp.appCode,
1181
+ skillId: Number(skillId)
1182
+ }).catch(() => {
1183
+ })
1184
+ message.success('工具配置已保存')
1185
+ } catch (e) {
1186
+ message.error('保存失败')
1187
+ } finally {
1188
+ setSaving(false)
1189
+ }
1190
+ }
1191
+
1192
+ const renderItemList = (title, icon, items, boundList, type, renderItem) => (
1193
+ <div>
1194
+ <div style={{display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16}}>
1195
+ {icon}<span style={{fontWeight: 500}}>{title}</span><Tag>{items.length}</Tag>
1196
+ </div>
1197
+ {items.length === 0 ? (<Empty description="暂无可用项" image={Empty.PRESENTED_IMAGE_SIMPLE}/>) : (
1198
+ <div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
1199
+ {items.map(renderItem)}
1200
+ </div>
1201
+ )}
1202
+ </div>
1203
+ )
1204
+
1205
+ if (apps.length === 0) return (
1206
+ <div style={{padding: 48, textAlign: 'center'}}><Empty description="请先创建应用后再配置工具"/></div>)
1207
+
1208
+ return (
1209
+ <div style={{padding: 24}}>
1210
+ <Row gutter={24}>
1211
+ <Col span={8}>
1212
+ <Title level={5} style={{marginBottom: 16}}>选择应用</Title>
1213
+ <div
1214
+ style={{background: '#fafafa', borderRadius: 8, padding: 16, maxHeight: 500, overflow: 'auto'}}>
1215
+ {apps.map(app => (
1216
+ <div key={app.id} onClick={() => setSelectedApp(app)} style={{
1217
+ padding: '12px 16px', marginBottom: 8, borderRadius: 8, cursor: 'pointer',
1218
+ background: selectedApp?.id === app.id ? '#fff' : 'transparent',
1219
+ border: selectedApp?.id === app.id ? '2px solid #667eea' : '2px solid transparent',
1220
+ transition: 'all 0.2s'
1221
+ }}>
1222
+ <div style={{display: 'flex', alignItems: 'center', gap: 10}}>
1223
+ <Avatar size="small" icon={<RobotOutlined/>}
1224
+ style={{background: app.status === 'PUBLISHED' ? 'linear-gradient(135deg, #52c41a, #389e0d)' : 'linear-gradient(135deg, #667eea, #764ba2)'}}/>
1225
+ <div>
1226
+ <div style={{fontWeight: 500, fontSize: 14}}>{app.appName}</div>
1227
+ <div style={{fontSize: 11, color: '#8c8c8c'}}>{app.appCode}</div>
1228
+ </div>
1229
+ </div>
1230
+ </div>
1231
+ ))}
1232
+ </div>
1233
+ </Col>
1234
+ <Col span={16}>
1235
+ {!selectedApp ? (
1236
+ <div style={{padding: 48, textAlign: 'center', background: '#fafafa', borderRadius: 12}}>
1237
+ <RobotOutlined style={{fontSize: 48, color: '#d9d9d9'}}/>
1238
+ <div style={{marginTop: 16, color: '#8c8c8c'}}>请从左侧选择一个应用</div>
1239
+ </div>) : (
1240
+ <div>
1241
+ <div style={{
1242
+ display: 'flex',
1243
+ alignItems: 'center',
1244
+ justifyContent: 'space-between',
1245
+ marginBottom: 20
1246
+ }}>
1247
+ <Title level={5} style={{margin: 0}}>配置 {selectedApp.appName}</Title>
1248
+ <Button type="primary" icon={<CheckCircleFilled/>} onClick={handleSave}
1249
+ loading={saving}>保存配置</Button>
1250
+ </div>
1251
+ <Tabs activeKey={activeTab} onChange={setActiveTab}>
1252
+ <TabPane tab="工具" key="tools">
1253
+ <div style={{
1254
+ padding: 16,
1255
+ background: '#fafafa',
1256
+ borderRadius: 8
1257
+ }}>{renderItemList('可用工具', <ToolOutlined/>, tools, boundTools, 'tool', (t) => (
1258
+ <div key={t.toolCode} onClick={() => toggleItem('tool', t.toolCode)} style={{
1259
+ padding: '10px 14px',
1260
+ background: isBound(boundTools, t.toolCode) ? '#e6f7ff' : '#fff',
1261
+ border: `1px solid ${isBound(boundTools, t.toolCode) ? '#91d5ff' : '#f0f0f0'}`,
1262
+ borderRadius: 6,
1263
+ cursor: 'pointer',
1264
+ display: 'flex',
1265
+ alignItems: 'center',
1266
+ gap: 10
1267
+ }}>
1268
+ <div style={{
1269
+ width: 20,
1270
+ height: 20,
1271
+ borderRadius: 4,
1272
+ background: isBound(boundTools, t.toolCode) ? '#1890ff' : '#d9d9d9',
1273
+ display: 'flex',
1274
+ alignItems: 'center',
1275
+ justifyContent: 'center'
1276
+ }}>{isBound(boundTools, t.toolCode) &&
1277
+ <CheckCircleFilled style={{color: '#fff', fontSize: 12}}/>}</div>
1278
+ <div>
1279
+ <div style={{
1280
+ fontWeight: 500,
1281
+ fontSize: 13
1282
+ }}>{t.toolName || t.toolCode}</div>
1283
+ <div style={{
1284
+ fontSize: 11,
1285
+ color: '#8c8c8c'
1286
+ }}>{t.description || t.toolCode}</div>
1287
+ </div>
1288
+ </div>))}</div>
1289
+ </TabPane>
1290
+ <TabPane tab="技能" key="skills">
1291
+ <div style={{
1292
+ padding: 16,
1293
+ background: '#fafafa',
1294
+ borderRadius: 8
1295
+ }}>{renderItemList('可用技能',
1296
+ <StarOutlined/>, skills, boundSkills, 'skill', (s) => (
1297
+ <div key={s.id} onClick={() => toggleItem('skill', String(s.id))} style={{
1298
+ padding: '10px 14px',
1299
+ background: isBound(boundSkills, String(s.id)) ? '#fff7e6' : '#fff',
1300
+ border: `1px solid ${isBound(boundSkills, String(s.id)) ? '#ffd591' : '#f0f0f0'}`,
1301
+ borderRadius: 6,
1302
+ cursor: 'pointer',
1303
+ display: 'flex',
1304
+ alignItems: 'center',
1305
+ gap: 10
1306
+ }}>
1307
+ <div style={{
1308
+ width: 20,
1309
+ height: 20,
1310
+ borderRadius: 4,
1311
+ background: isBound(boundSkills, String(s.id)) ? '#fa8c16' : '#d9d9d9',
1312
+ display: 'flex',
1313
+ alignItems: 'center',
1314
+ justifyContent: 'center'
1315
+ }}>{isBound(boundSkills, String(s.id)) &&
1316
+ <CheckCircleFilled style={{color: '#fff', fontSize: 12}}/>}</div>
1317
+ <div>
1318
+ <div style={{fontWeight: 500, fontSize: 13}}>{s.name}</div>
1319
+ <div style={{
1320
+ fontSize: 11,
1321
+ color: '#8c8c8c'
1322
+ }}>{s.description || s.code}</div>
1323
+ </div>
1324
+ </div>))}</div>
1325
+ </TabPane>
1326
+ <TabPane tab="知识库" key="knowledge">
1327
+ <div style={{
1328
+ padding: 16,
1329
+ background: '#fafafa',
1330
+ borderRadius: 8
1331
+ }}>{renderItemList('可用知识库',
1332
+ <BookOutlined/>, knowledge, boundKnowledge, 'kb', (k) => (
1333
+ <div key={k.id} onClick={() => toggleItem('kb', String(k.id))} style={{
1334
+ padding: '10px 14px',
1335
+ background: isBound(boundKnowledge, String(k.id)) ? '#f6ffed' : '#fff',
1336
+ border: `1px solid ${isBound(boundKnowledge, String(k.id)) ? '#b7eb8f' : '#f0f0f0'}`,
1337
+ borderRadius: 6,
1338
+ cursor: 'pointer',
1339
+ display: 'flex',
1340
+ alignItems: 'center',
1341
+ gap: 10
1342
+ }}>
1343
+ <div style={{
1344
+ width: 20,
1345
+ height: 20,
1346
+ borderRadius: 4,
1347
+ background: isBound(boundKnowledge, String(k.id)) ? '#52c41a' : '#d9d9d9',
1348
+ display: 'flex',
1349
+ alignItems: 'center',
1350
+ justifyContent: 'center'
1351
+ }}>{isBound(boundKnowledge, String(k.id)) &&
1352
+ <CheckCircleFilled style={{color: '#fff', fontSize: 12}}/>}</div>
1353
+ <div>
1354
+ <div style={{fontWeight: 500, fontSize: 13}}>{k.name}</div>
1355
+ <div style={{
1356
+ fontSize: 11,
1357
+ color: '#8c8c8c'
1358
+ }}>{k.description || `ID: ${k.id}`}</div>
1359
+ </div>
1360
+ </div>))}</div>
1361
+ </TabPane>
1362
+ </Tabs>
1363
+ </div>
1364
+ )}
1365
+ </Col>
1366
+ </Row>
1367
+ </div>
1368
+ )
1369
+ }
1370
+
1371
+ // ===== 分享管理页 =====
1372
+ const ShareManagementPage = () => (
1373
+ <div style={{padding: 48, textAlign: 'center'}}>
1374
+ <ShareAltOutlined style={{fontSize: 48, color: '#d9d9d9'}}/>
1375
+ <div style={{marginTop: 16, color: '#8c8c8c'}}>分享管理功能开发中...</div>
1376
+ </div>
1377
+ )
1378
+
1379
+ export default AgentAppPage