From 86c923cc406db86469841c6bdfbb03cfb7ec7002 Mon Sep 17 00:00:00 2001 From: sp mac bookpro 2605 Date: Sun, 19 Jul 2026 23:01:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=80=E6=94=BE=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E5=A4=96=E9=83=A8=E5=B7=A5=E5=85=B7=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/agents.ts | 36 ++- src/components/ExternalToolEditor.tsx | 229 ++++++++++++++++++ .../components/CapabilitySettings.tsx | 18 ++ .../capability/KnowledgeSettingsPanel.tsx | 172 ++++++++++++- src/pages/AgentEditor/hooks/useAgentEditor.ts | 24 ++ src/pages/AgentEditor/index.tsx | 52 +++- .../styles/agent-editor-capability.css | 132 ++++++++++ 7 files changed, 642 insertions(+), 21 deletions(-) create mode 100644 src/components/ExternalToolEditor.tsx diff --git a/src/api/agents.ts b/src/api/agents.ts index ff3bfa5..90cfbcb 100644 --- a/src/api/agents.ts +++ b/src/api/agents.ts @@ -33,6 +33,32 @@ export interface SkillDetail extends SkillBrief { config: string; } +export interface ExternalToolApi { + id?: string; + name: string; + description: string; + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + path: string; + headers?: Record | null; + parametersSchema: Record; + createdAt?: number; +} + +export interface ExternalToolPluginPayload { + name: string; + description?: string; + baseUrl: string; + authType: 'none' | 'bearer' | 'basic' | 'apiKey'; + authConfig: Record; + apis: ExternalToolApi[]; +} + +export interface ExternalToolPlugin extends ExternalToolPluginPayload { + id: string; + enabled: number; + createdAt: number; +} + export interface Agent { id: string; name: string; @@ -50,6 +76,7 @@ export interface Agent { updated_at: number; knowledge?: KnowledgeFile[]; skills?: SkillBrief[]; + plugins?: ExternalToolPlugin[]; _access?: 'owner' | 'team' | 'view' | 'none'; } @@ -83,6 +110,13 @@ export const AgentAPI = { getSkill: (agentId: string, skillId: string) => api.get(`/agents/${agentId}/skills/${skillId}`).then((r) => r.data), updateSkill: (agentId: string, skillId: string, payload: { content?: string; enabled?: boolean }) => api.put(`/agents/${agentId}/skills/${skillId}`, payload).then((r) => r.data), - deleteSkill: (agentId: string, skillId: string) => api.delete(`/agents/${agentId}/skills/${skillId}`).then((r) => r.data) + deleteSkill: (agentId: string, skillId: string) => api.delete(`/agents/${agentId}/skills/${skillId}`).then((r) => r.data), + + bindPlugin: (agentId: string, payload: ExternalToolPluginPayload) => + api.post(`/agents/${agentId}/plugins`, payload).then((r) => r.data), + updatePlugin: (agentId: string, pluginId: string, payload: ExternalToolPluginPayload) => + api.put(`/agents/${agentId}/plugins/${pluginId}`, payload).then((r) => r.data), + deletePlugin: (agentId: string, pluginId: string) => + api.delete(`/agents/${agentId}/plugins/${pluginId}`).then((r) => r.data) }; diff --git a/src/components/ExternalToolEditor.tsx b/src/components/ExternalToolEditor.tsx new file mode 100644 index 0000000..5454600 --- /dev/null +++ b/src/components/ExternalToolEditor.tsx @@ -0,0 +1,229 @@ +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { App as AntApp, Button, Card, Form, Input, Modal, Select, Space } from 'antd'; +import { AgentAPI, ExternalToolApi, ExternalToolPlugin, ExternalToolPluginPayload } from '../api'; + +interface Props { + open: boolean; + agentId: string; + plugin?: ExternalToolPlugin | null; + onClose: () => void; + onSaved?: () => void | Promise; +} + +interface ToolApiFormValue extends Omit { + headers?: string; + parametersSchema: string; +} + +interface ToolPluginFormValue extends Omit { + authConfig?: string; + apis: ToolApiFormValue[]; +} + +const EMPTY_API: ToolApiFormValue = { + name: '', + description: '', + method: 'GET', + path: '', + headers: '{}', + parametersSchema: JSON.stringify({ type: 'object', properties: {} }, null, 2), +}; + +function parseJsonObject(value: string | undefined, fieldName: string, optional = false) { + if (!value?.trim()) { + if (optional) return undefined; + throw new Error(`${fieldName}不能为空`); + } + + const parsed = JSON.parse(value); + if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') { + throw new Error(`${fieldName}必须是 JSON 对象`); + } + return parsed; +} + +export default function ExternalToolEditor({ open, agentId, plugin, onClose, onSaved }: Props) { + const { message } = AntApp.useApp(); + const [form] = Form.useForm(); + const isEditing = Boolean(plugin); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const payload: ExternalToolPluginPayload = { + name: values.name.trim(), + description: values.description?.trim(), + baseUrl: values.baseUrl.trim(), + authType: values.authType, + authConfig: parseJsonObject(values.authConfig, '认证配置', values.authType === 'none') || {}, + apis: values.apis.map((item) => ({ + name: item.name.trim(), + description: item.description.trim(), + method: item.method, + path: item.path.trim(), + headers: parseJsonObject(item.headers, `API ${item.name} 的请求头`, true), + parametersSchema: parseJsonObject(item.parametersSchema, `API ${item.name} 的依赖参数`), + })), + }; + + if (plugin) { + await AgentAPI.updatePlugin(agentId, plugin.id, payload); + message.success('外部工具更新成功'); + } else { + await AgentAPI.bindPlugin(agentId, payload); + message.success('外部工具绑定成功'); + } + await onSaved?.(); + onClose(); + } catch (error: any) { + if (error?.errorFields) return; + if (error instanceof SyntaxError) { + message.error('JSON 格式不正确,请检查认证配置、请求头或依赖参数'); + return; + } + message.error(error?.message || '外部工具绑定失败'); + } + }; + + return ( + { + if (visible) { + form.setFieldsValue( + plugin + ? { + name: plugin.name, + description: plugin.description, + baseUrl: plugin.baseUrl, + authType: plugin.authType, + authConfig: JSON.stringify(plugin.authConfig || {}, null, 2), + apis: plugin.apis.map((item) => ({ + name: item.name, + description: item.description, + method: item.method, + path: item.path, + headers: JSON.stringify(item.headers || {}, null, 2), + parametersSchema: JSON.stringify(item.parametersSchema || { type: 'object', properties: {} }, null, 2), + })), + } + : { + name: '', + description: '', + baseUrl: '', + authType: 'bearer', + authConfig: JSON.stringify({ token: '' }, null, 2), + apis: [{ ...EMPTY_API }], + }, + ); + } + }} + > +
+
+ + + + + + +
+ + + +
+ + + + + + +
+ + + + + + +
+ + ))} + + )} + + + + ); +} diff --git a/src/pages/AgentEditor/components/CapabilitySettings.tsx b/src/pages/AgentEditor/components/CapabilitySettings.tsx index 0506c23..666379e 100644 --- a/src/pages/AgentEditor/components/CapabilitySettings.tsx +++ b/src/pages/AgentEditor/components/CapabilitySettings.tsx @@ -17,6 +17,12 @@ interface CapabilitySettingsProps { setAvatarSelectorOpen: (open: boolean) => void; beforeUploadKnowledge: (file: any) => Promise; onDeleteKnowledge: (fileId: string) => Promise; + onCreateSkill: () => void; + onEditSkill: (skillId: string) => void; + onDeleteSkill: (skillId: string) => Promise; + onCreateExternalTool: () => void; + onEditExternalTool: (pluginId: string) => void; + onDeleteExternalTool: (pluginId: string) => Promise; markDirty: () => void; isMobile?: boolean; } @@ -32,6 +38,12 @@ export default function CapabilitySettings({ setAvatarSelectorOpen, beforeUploadKnowledge, onDeleteKnowledge, + onCreateSkill, + onEditSkill, + onDeleteSkill, + onCreateExternalTool, + onEditExternalTool, + onDeleteExternalTool, markDirty, isMobile = false, }: CapabilitySettingsProps) { @@ -62,6 +74,12 @@ export default function CapabilitySettings({ agent={agent} beforeUploadKnowledge={beforeUploadKnowledge} onDeleteKnowledge={onDeleteKnowledge} + onCreateSkill={onCreateSkill} + onEditSkill={onEditSkill} + onDeleteSkill={onDeleteSkill} + onCreateExternalTool={onCreateExternalTool} + onEditExternalTool={onEditExternalTool} + onDeleteExternalTool={onDeleteExternalTool} /> diff --git a/src/pages/AgentEditor/components/capability/KnowledgeSettingsPanel.tsx b/src/pages/AgentEditor/components/capability/KnowledgeSettingsPanel.tsx index c3df48c..a5c83a7 100644 --- a/src/pages/AgentEditor/components/capability/KnowledgeSettingsPanel.tsx +++ b/src/pages/AgentEditor/components/capability/KnowledgeSettingsPanel.tsx @@ -1,5 +1,5 @@ -import { Button, Collapse, Form, Input, List, Popconfirm, Tag } from 'antd'; -import { DatabaseOutlined, ToolOutlined } from '@ant-design/icons'; +import { ApiOutlined, DatabaseOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ToolOutlined } from '@ant-design/icons'; +import { Button, Card, Collapse, Input, List, Popconfirm, Space, Tag } from 'antd'; import { Agent } from '../../../../api'; import { STATUS_TAG } from '../../constants'; @@ -7,13 +7,28 @@ interface KnowledgeSettingsPanelProps { agent: Agent | null; beforeUploadKnowledge: (file: any) => Promise; onDeleteKnowledge: (fileId: string) => Promise; + onCreateSkill: () => void; + onEditSkill: (skillId: string) => void; + onDeleteSkill: (skillId: string) => Promise; + onCreateExternalTool: () => void; + onEditExternalTool: (pluginId: string) => void; + onDeleteExternalTool: (pluginId: string) => Promise; } export default function KnowledgeSettingsPanel({ agent, beforeUploadKnowledge, onDeleteKnowledge, + onCreateSkill, + onEditSkill, + onDeleteSkill, + onCreateExternalTool, + onEditExternalTool, + onDeleteExternalTool, }: KnowledgeSettingsPanelProps) { + const skills = agent?.skills ?? []; + const plugins = agent?.plugins ?? []; + return ( { - const files = e.target.files; + onChange={async (event) => { + const files = event.target.files; if (!files) return; - for (let i = 0; i < files.length; i++) { - await beforeUploadKnowledge(files[i]); + for (let index = 0; index < files.length; index++) { + await beforeUploadKnowledge(files[index]); } + event.target.value = ''; }} /> + + +
+ +
+
+ Skills + {skills.length} +
+ ( + } onClick={() => onEditSkill(item.id)}> + 编辑 + , + onDeleteSkill(item.id)}> + + , + ]} + > + {item.filename || item.name}} + description={ +
+
{item.description || '暂无描述'}
+ {item.filename && item.filename !== item.name &&
{item.name}
} +
+ } + /> + + {item.type} + {item.enabled ? '已启用' : '未启用'} + +
+ )} + /> +
+ +
+
+ 外部工具集 + {plugins.length} +
+ {plugins.length === 0 ? ( +
暂无外部工具集
+ ) : ( + + {plugins.map((plugin) => ( + + + {plugin.name} + {plugin.enabled ? '已启用' : '未启用'} + + } + extra={ + + + onDeleteExternalTool(plugin.id)}> + + + + } + > +
{plugin.description || '暂无描述'}
+ + {plugin.authType || 'none'} + {plugin.baseUrl} + {plugin.apis.length} 个 API + + ({ + key: api.id || api.name, + label: ( + + {api.method} + {api.name} + {api.path} + + ), + children: ( +
+
{api.description || '暂无描述'}
+
+
+ headers +
{JSON.stringify(api.headers || {}, null, 2)}
+
+
+ parametersSchema +
{JSON.stringify(api.parametersSchema || {}, null, 2)}
+
+
+
+ ), + }))} + /> + + ))} + + )} +
), - children: null, }, ]} /> diff --git a/src/pages/AgentEditor/hooks/useAgentEditor.ts b/src/pages/AgentEditor/hooks/useAgentEditor.ts index e913c9c..3d0c4a1 100644 --- a/src/pages/AgentEditor/hooks/useAgentEditor.ts +++ b/src/pages/AgentEditor/hooks/useAgentEditor.ts @@ -223,6 +223,28 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE } }; + const handleDeleteSkill = async (skillId: string) => { + if (!id) return; + try { + await AgentAPI.deleteSkill(id, skillId); + setAgent((prev) => prev ? { ...prev, skills: (prev.skills || []).filter((skill) => skill.id !== skillId) } : prev); + message.success('Skill 已删除'); + } catch (e: any) { + message.error('Skill 删除失败:' + (e?.message ?? e)); + } + }; + + const handleDeletePlugin = async (pluginId: string) => { + if (!id) return; + try { + await AgentAPI.deletePlugin(id, pluginId); + setAgent((prev) => prev ? { ...prev, plugins: (prev.plugins || []).filter((plugin) => plugin.id !== pluginId) } : prev); + message.success('外部工具已删除'); + } catch (e: any) { + message.error('外部工具删除失败:' + (e?.message ?? e)); + } + }; + const beforeUploadKnowledge = async (file: any) => { if (!id) { message.warning('请先保存智能体基础信息后再上传'); @@ -328,6 +350,8 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE beforeUploadEditAvatar, handleAvatarSelect, handleDeleteKnowledge, + handleDeleteSkill, + handleDeletePlugin, liveAgent, currentName, markDirty, diff --git a/src/pages/AgentEditor/index.tsx b/src/pages/AgentEditor/index.tsx index d6b8643..5c5e5fd 100644 --- a/src/pages/AgentEditor/index.tsx +++ b/src/pages/AgentEditor/index.tsx @@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { App as AntApp } from 'antd'; import { FileTextOutlined, SaveOutlined } from '@ant-design/icons'; import SkillEditor from '../../components/SkillEditor'; +import ExternalToolEditor from '../../components/ExternalToolEditor'; import { useAgentEditor } from './hooks/useAgentEditor'; import { useIsMobile } from '../../hooks/useIsMobile'; import Header from './components/Header'; @@ -24,6 +25,8 @@ export default function AgentEditor() { const navigate = useNavigate(); const { message } = AntApp.useApp(); const [form] = Form.useForm(); + const [externalToolEditorOpen, setExternalToolEditorOpen] = useState(false); + const [editingPluginId, setEditingPluginId] = useState(null); const { agent, @@ -52,6 +55,8 @@ export default function AgentEditor() { beforeUploadEditAvatar, handleAvatarSelect, handleDeleteKnowledge, + handleDeleteSkill, + handleDeletePlugin, liveAgent, currentName, markDirty, @@ -85,6 +90,24 @@ export default function AgentEditor() { setAvatarSelectorOpen={setAvatarSelectorOpen} beforeUploadKnowledge={beforeUploadKnowledge} onDeleteKnowledge={handleDeleteKnowledge} + onCreateSkill={() => { + setEditingSkillId(null); + setSkillEditorOpen(true); + }} + onEditSkill={(skillId) => { + setEditingSkillId(skillId); + setSkillEditorOpen(true); + }} + onDeleteSkill={handleDeleteSkill} + onCreateExternalTool={() => { + setEditingPluginId(null); + setExternalToolEditorOpen(true); + }} + onEditExternalTool={(pluginId) => { + setEditingPluginId(pluginId); + setExternalToolEditorOpen(true); + }} + onDeleteExternalTool={handleDeletePlugin} markDirty={markDirty} isMobile={isMobile} /> @@ -120,13 +143,28 @@ export default function AgentEditor() { )} {!isNew && ( - setSkillEditorOpen(false)} - onSaved={refresh} - /> + <> + { + setSkillEditorOpen(false); + setEditingSkillId(null); + }} + onSaved={refresh} + /> + plugin.id === editingPluginId)} + onClose={() => { + setExternalToolEditorOpen(false); + setEditingPluginId(null); + }} + onSaved={refresh} + /> + )} diff --git a/src/pages/AgentEditor/styles/agent-editor-capability.css b/src/pages/AgentEditor/styles/agent-editor-capability.css index 42cb5d4..1487de8 100644 --- a/src/pages/AgentEditor/styles/agent-editor-capability.css +++ b/src/pages/AgentEditor/styles/agent-editor-capability.css @@ -72,6 +72,138 @@ color: var(--color-brand); } +.agent-editor-skill-actions, +.agent-editor-tool-list-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.875rem; +} + +.agent-editor-skill-item { + margin-bottom: 0.5rem; + padding: 0.625rem 0.75rem !important; + border: 1px solid #edf1f7; + border-radius: 0.75rem; + background: #fff; +} + +.agent-editor-skill-name { + font-weight: 700; +} + +.agent-editor-tool-section + .agent-editor-tool-section { + margin-top: 1.25rem; +} + +.agent-editor-tool-section-title { + display: flex; + align-items: center; + gap: 0.375rem; + margin-bottom: 0.625rem; + color: var(--color-text); + font-size: 0.8125rem; + font-weight: 800; +} + +.agent-editor-tool-empty { + padding: 1.25rem; + border: 1px dashed #d9e1ec; + border-radius: 0.75rem; + color: var(--color-text-secondary); + text-align: center; +} + +.agent-editor-plugin-card { + border-color: #e7edf6; + border-radius: 0.875rem; +} + +.agent-editor-plugin-description { + margin-bottom: 0.5rem; + color: var(--color-text-secondary); + font-size: 0.8125rem; +} + +.agent-editor-plugin-meta { + color: var(--color-text-secondary); + font-size: 0.75rem; +} + +.agent-editor-plugin-apis { + margin-top: 0.625rem; + border-top: 1px solid #edf1f7; +} + +.agent-editor-api-detail { + color: var(--color-text-secondary); + font-size: 0.78125rem; +} + +.agent-editor-api-config-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 0.75rem; + margin-top: 0.625rem; +} + +.agent-editor-api-config-grid pre { + max-height: 12rem; + margin: 0.25rem 0 0; + padding: 0.625rem; + overflow: auto; + border-radius: 0.5rem; + background: #f6f8fb; + color: #334155; + font-size: 0.6875rem; + white-space: pre-wrap; + word-break: break-word; +} + +.agent-editor-tool-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 0.875rem; +} + +.agent-editor-tool-list { + width: 100%; +} + +.agent-editor-tool-list-header { + margin: 0; +} + +.agent-editor-tool-help { + margin-top: 0.125rem; + color: var(--color-text-secondary); + font-size: 0.75rem; +} + +.agent-editor-tool-card { + border-radius: 0.875rem; +} + +.agent-editor-code-input { + font-family: Consolas, Menlo, Monaco, monospace; + font-size: 0.75rem; +} + +@media (max-width: 768px) { + .agent-editor-tool-grid, + .agent-editor-api-config-grid { + grid-template-columns: 1fr; + gap: 0; + } + + .agent-editor-skill-actions, + .agent-editor-tool-list-header { + align-items: flex-start; + flex-direction: column; + } +} + .agent-editor-field-input, .agent-editor-number-input { height: 2.625rem;