import { useEffect, useRef, useState } from 'react'; import { Button, Form, Input, InputNumber, Modal, Upload, App as AntApp, List, Popconfirm, Tag, Switch, Select, Collapse } from 'antd'; import type { UploadFile } from 'antd/es/upload/interface'; import { useNavigate, useParams } from 'react-router-dom'; import { Agent, AgentAPI, ImageAPI, KnowledgeStatus, SkillType, Team, TeamAPI } from '../api'; import SkillEditor from '../components/SkillEditor'; import McpPanel from '../components/McpPanel'; import ChatPreview from '../components/ChatPreview'; import { ArrowLeftOutlined, SaveOutlined, FileTextOutlined, RocketOutlined, ToolOutlined, DatabaseOutlined, SettingOutlined, UploadOutlined } from '@ant-design/icons'; const STATUS_TAG: Record = { pending: { color: 'default', text: '待处理' }, indexing: { color: 'processing', text: '索引中…' }, ready: { color: 'success', text: '已就绪' }, failed: { color: 'error', text: '失败' }, }; const TYPE_TAG: Record = { prompt: { color: 'blue', icon: '📝', label: 'Prompt' }, http: { color: 'green', icon: '🌐', label: 'HTTP' }, js: { color: 'volcano', icon: '⚙️', label: 'JS' }, }; const DEFAULT_AVATAR = '/default_bot_icon.jpg'; const PRESET_AVATARS: string[] = Array.from({ length: 12 }, (_, index) => `/avatars/avatar-${String(index + 1).padStart(2, '0')}.png`); const isImageUrl = (url: string) => url?.startsWith('http') || url?.startsWith('/'); export default function AgentEditor() { const { id } = useParams(); const isNew = !id; const navigate = useNavigate(); const { message } = AntApp.useApp(); const [form] = Form.useForm(); const [initForm] = Form.useForm(); const [agent, setAgent] = useState(null); const [saving, setSaving] = useState(false); const [teams, setTeams] = useState([]); const [autoSaveStatus, setAutoSaveStatus] = useState<'saved' | 'saving' | 'error'>('saved'); const [initModalOpen, setInitModalOpen] = useState(isNew); const [selectedAvatar, setSelectedAvatar] = useState(DEFAULT_AVATAR); const [avatarSelectorOpen, setAvatarSelectorOpen] = useState(false); const [avatarUploading, setAvatarUploading] = useState(false); // 监听名称变化以同步头像首字母 const [agentName, setAgentName] = useState(''); // skill editor const [skillEditorOpen, setSkillEditorOpen] = useState(false); const [editingSkillId, setEditingSkillId] = useState(null); const pollTimer = useRef(null); const refresh = async () => { if (!id) return; const data = await AgentAPI.detail(id); setAgent(data); form.setFieldsValue(data); setAgentName(data.name); setSelectedAvatar(data.avatar || DEFAULT_AVATAR); // 若有索引中文件 → 启动轮询 const indexing = data.knowledge?.some((k) => k.status === 'pending' || k.status === 'indexing'); if (indexing && !pollTimer.current) { pollTimer.current = window.setInterval(refresh, 2000); } else if (!indexing && pollTimer.current) { window.clearInterval(pollTimer.current); pollTimer.current = null; } }; useEffect(() => { TeamAPI.list() .then(setTeams) .catch(() => setTeams([])); if (isNew) { setInitModalOpen(true); setSelectedAvatar(DEFAULT_AVATAR); setAgentName(''); form.setFieldsValue({ name: '', description: '', prompt: 'You are a helpful AI assistant.', model: '', temperature: 0.7, visibility: 'private', teamId: null, }); } else { setInitModalOpen(false); refresh(); } return () => { if (pollTimer.current) { window.clearInterval(pollTimer.current); pollTimer.current = null; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); const handleInitConfirm = async () => { const values = await initForm.validateFields(); setSaving(true); try { const created = await AgentAPI.create({ ...values, avatar: selectedAvatar, prompt: 'You are a helpful AI assistant.', temperature: 0.7, visibility: 'private', }); message.success('初始化成功'); setInitModalOpen(false); navigate(`/agents/${created.id}`, { replace: true }); } catch (e) { message.error('创建失败'); } finally { setSaving(false); } }; const handleSave = async (silent = false) => { if (isNew) return; // 初始弹窗未确认前不触发自动保存 const values = await form.validateFields(); if (!silent) setSaving(true); setAutoSaveStatus('saving'); try { await AgentAPI.update(id!, values); if (!silent) message.success('已保存'); refresh(); setAutoSaveStatus('saved'); } catch (e) { setAutoSaveStatus('error'); if (!silent) message.error('保存失败'); } finally { if (!silent) setSaving(false); } }; const beforeUploadKnowledge = async (file: UploadFile) => { if (!id) { message.warning('请先保存智能体基础信息后再上传'); return Upload.LIST_IGNORE; } try { await AgentAPI.uploadKnowledge(id, [file as unknown as File]); message.success(`${file.name} 已上传,正在建索引…`); refresh(); } catch (e: any) { message.error('上传失败:' + (e?.message ?? e)); } return Upload.LIST_IGNORE; }; const uploadAvatar = async (file: File) => { setAvatarUploading(true); try { const res = await ImageAPI.upload([file]); const url = res.files?.[0]?.url; if (!url) { throw new Error('未获取到图片地址'); } return url; } finally { setAvatarUploading(false); } }; const beforeUploadInitAvatar = async (file: UploadFile) => { try { const url = await uploadAvatar(file as unknown as File); setSelectedAvatar(url); message.success('头像上传成功'); } catch (e: any) { message.error('头像上传失败:' + (e?.message ?? e)); } return Upload.LIST_IGNORE; }; const beforeUploadEditAvatar = async (file: UploadFile) => { if (!id) return Upload.LIST_IGNORE; try { const url = await uploadAvatar(file as unknown as File); await AgentAPI.update(id, { avatar: url }); message.success('头像已更新'); setAvatarSelectorOpen(false); refresh(); } catch (e: any) { message.error('头像上传失败:' + (e?.message ?? e)); } return Upload.LIST_IGNORE; }; const liveAgent = agent || (form.getFieldsValue() as Agent); const currentName = liveAgent?.name || agentName || '未命名智能体'; return ( <>
{/* Header */}
{autoSaveStatus === 'saving' ? '正在保存...' : '更改已自动保存'}
{/* Main Content */}
{/* Left Column: Personalization (System Prompt) */}

个性化

定义这个智能体的身份、语气、边界和输出规范,让它更像一个稳定的角色,而不是随机回复的模型。

System Prompt
提示词是这个智能体最重要的灵魂设定
可以从身份定位、擅长任务、回应风格、拒答边界和输出格式这五个维度去描述,让预览区的表现更稳定。
handleSave(true)} >
建议写清楚角色设定、目标用户、语气和回答结构。
提示:写得越具体,预览区里的回答风格越稳定。
{/* Middle Column: Capabilities & Basic Info */}

能力与设置

这里决定它用什么模型、拥有哪些知识和技能,以及它会以怎样的方式被别人看到。

Capability
handleSave(true)} >
setAvatarSelectorOpen(true)} > {isImageUrl(agent?.avatar || selectedAvatar) ? ( avatar ) : ( (agentName?.charAt(0) || '?').toUpperCase() )}
更换形象
{currentName}
{liveAgent?.description || '补充基础资料后,这里会更像一个完整可运营的产品角色。'}
基础设置
), children: (
名称、描述和可见性决定了这个智能体如何被理解和管理。
setAgentName(e.target.value)} style={{ borderRadius: 12, height: 42 }} />
({ value: t.id, label: t.name }))} />
), }, ]} />
模型设置
控制随机性,数值越高越发散
知识库 ({agent?.knowledge?.length ?? 0})
), children: (
上传文档以增强 AI 知识
( AgentAPI.deleteKnowledge(id!, item.id).then(refresh)} > , ]} >
{item.originalName} {(item.size / 1024).toFixed(1)} KB ·{' '} {STATUS_TAG[item.status].text}
)} />
), }, { key: 'skills', label: (
技能 & 工具 ({agent?.skills?.length ?? 0})
), children: (
通过工具扩展 AI 功能
( AgentAPI.updateSkill(id!, item.id, { enabled: v }).then(refresh)} />, , ]} >
{TYPE_TAG[item.type].label} {item.name}
)} />
), }, { key: 'mcp', label: (
MCP 集成
), children: id ? :
保存后启用 MCP
, }, ]} />
联网搜索
启用后,智能体可以在回答时通过 DuckDuckGo 搜索实时信息。
{/* Right Column: Preview */}

预览

一边调整人设和能力,一边看对话气质是否符合你的预期。

Live Preview
当前预览角色
{currentName} 会基于左侧 Prompt 与中栏设置立即更新。你可以直接在这里检查语气、头像和整体观感。
{!isNew && ( setSkillEditorOpen(false)} onSaved={refresh} /> )} navigate('/marketplace')} footer={null} width={720} centered maskClosable={false} destroyOnHidden >
第一步 · 定义智能体形象
先给你的智能体一个更完整的开场
先决定它的形象、名字和一句话定位。确认后会进入三栏工作台,继续完成个性化、能力配置和实时预览。
{isImageUrl(selectedAvatar) ? ( avatar ) : ( (agentName?.charAt(0) || '?').toUpperCase() )}
{agentName || '你的新智能体'}
这里会实时映射你输入的名字与选择的形象。
{ if (changed.name !== undefined) setAgentName(changed.name); }} className="agent-editor-modal-card" style={{ padding: 20 }} > 智能体名称 } rules={[{ required: true, message: '请输入智能体名称' }]} > 描述(选填) } >
{['客服助理', '内容创作', '数据分析', '私人教练'].map((label) => ( { // 给 initForm 赋值(因为第一步向导是 initForm,不是主页面的 form) initForm.setFieldsValue({ description: label }); }} style={{ padding: '6px 10px', borderRadius: 999, background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)', fontSize: 12.5, cursor: 'pointer', transition: 'all 0.2s', }} onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--color-border)'; e.currentTarget.style.color = 'var(--color-text)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--color-surface-2)'; e.currentTarget.style.color = 'var(--color-text-secondary)'; }} > {label} ))}
选择你的智能体形象
默认会使用系统头像,你也可以上传自己的图片替换。
{[DEFAULT_AVATAR, ...PRESET_AVATARS].map((url) => (
setSelectedAvatar(url)} className={`relative aspect-square rounded-full cursor-pointer transition-all duration-300 overflow-hidden border-2 ${selectedAvatar === url ? 'scale-110 shadow-lg z-10' : 'border-transparent opacity-70 hover:opacity-100 hover:scale-105'}`} style={{ borderColor: selectedAvatar === url ? 'var(--color-brand)' : 'transparent' }} > preset {selectedAvatar === url && (
)}
))}
setAvatarSelectorOpen(false)} footer={null} width={520} centered >
点击即可更换
{/* 默认与内置图片 */} {[DEFAULT_AVATAR, ...PRESET_AVATARS].map((url) => (
{ await AgentAPI.update(id!, { avatar: url }); setAvatarSelectorOpen(false); refresh(); }} className={`relative aspect-square rounded-full cursor-pointer transition-all duration-300 overflow-hidden border-2 ${agent?.avatar === url ? 'scale-110 shadow-lg z-10' : 'border-transparent opacity-70 hover:opacity-100 hover:scale-105'}`} style={{ borderColor: agent?.avatar === url ? 'var(--color-brand)' : 'transparent' }} > preset {agent?.avatar === url && (
)}
))}
); }