feat: 开放并完善外部工具配置管理功能

main
sp mac bookpro 2605 2026-07-19 23:01:42 +08:00
parent b7c4159f1a
commit 86c923cc40
7 changed files with 642 additions and 21 deletions

View File

@ -33,6 +33,32 @@ export interface SkillDetail extends SkillBrief {
config: string; config: string;
} }
export interface ExternalToolApi {
id?: string;
name: string;
description: string;
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string;
headers?: Record<string, string> | null;
parametersSchema: Record<string, unknown>;
createdAt?: number;
}
export interface ExternalToolPluginPayload {
name: string;
description?: string;
baseUrl: string;
authType: 'none' | 'bearer' | 'basic' | 'apiKey';
authConfig: Record<string, unknown>;
apis: ExternalToolApi[];
}
export interface ExternalToolPlugin extends ExternalToolPluginPayload {
id: string;
enabled: number;
createdAt: number;
}
export interface Agent { export interface Agent {
id: string; id: string;
name: string; name: string;
@ -50,6 +76,7 @@ export interface Agent {
updated_at: number; updated_at: number;
knowledge?: KnowledgeFile[]; knowledge?: KnowledgeFile[];
skills?: SkillBrief[]; skills?: SkillBrief[];
plugins?: ExternalToolPlugin[];
_access?: 'owner' | 'team' | 'view' | 'none'; _access?: 'owner' | 'team' | 'view' | 'none';
} }
@ -83,6 +110,13 @@ export const AgentAPI = {
getSkill: (agentId: string, skillId: string) => api.get<SkillDetail>(`/agents/${agentId}/skills/${skillId}`).then((r) => r.data), getSkill: (agentId: string, skillId: string) => api.get<SkillDetail>(`/agents/${agentId}/skills/${skillId}`).then((r) => r.data),
updateSkill: (agentId: string, skillId: string, payload: { content?: string; enabled?: boolean }) => updateSkill: (agentId: string, skillId: string, payload: { content?: string; enabled?: boolean }) =>
api.put(`/agents/${agentId}/skills/${skillId}`, payload).then((r) => r.data), 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)
}; };

View File

@ -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<void>;
}
interface ToolApiFormValue extends Omit<ExternalToolApi, 'headers' | 'parametersSchema'> {
headers?: string;
parametersSchema: string;
}
interface ToolPluginFormValue extends Omit<ExternalToolPluginPayload, 'authConfig' | 'apis'> {
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<ToolPluginFormValue>();
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 (
<Modal
open={open}
title={isEditing ? '编辑外部工具集' : '配置外部工具集'}
width={920}
okText={isEditing ? '保存修改' : '绑定工具'}
cancelText="取消"
onCancel={onClose}
onOk={handleSubmit}
destroyOnHidden
afterOpenChange={(visible) => {
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 }],
},
);
}
}}
>
<Form form={form} layout="vertical" requiredMark="optional">
<div className="agent-editor-tool-grid">
<Form.Item label="工具集名称" name="name" rules={[{ required: true, message: '请输入工具集名称' }]}>
<Input placeholder="例如Hoyidata 工具集" />
</Form.Item>
<Form.Item label="API 基础地址" name="baseUrl" rules={[{ required: true, message: '请输入 API 基础地址' }]}>
<Input placeholder="https://api.hoyidata.com" />
</Form.Item>
</div>
<Form.Item label="工具集描述" name="description">
<Input.TextArea rows={2} placeholder="说明该工具集提供的能力" />
</Form.Item>
<div className="agent-editor-tool-grid">
<Form.Item label="认证方式" name="authType" rules={[{ required: true }]}>
<Select
options={[
{ value: 'none', label: '无需认证' },
{ value: 'bearer', label: 'Bearer Token' },
{ value: 'basic', label: 'Basic Auth' },
{ value: 'apiKey', label: 'API Key' },
]}
/>
</Form.Item>
<Form.Item label="认证配置JSON" name="authConfig">
<Input.TextArea rows={3} className="agent-editor-code-input" placeholder={'{\n "token": "YOUR_API_KEY"\n}'} />
</Form.Item>
</div>
<Form.List name="apis">
{(fields, { add, remove }) => (
<Space direction="vertical" size={12} className="agent-editor-tool-list">
<div className="agent-editor-tool-list-header">
<div>
<strong>API </strong>
<div className="agent-editor-tool-help"> API </div>
</div>
<Button type="dashed" icon={<PlusOutlined />} onClick={() => add({ ...EMPTY_API })}>
API
</Button>
</div>
{fields.map((field, index) => (
<Card
key={field.key}
size="small"
title={`API ${index + 1}`}
className="agent-editor-tool-card"
extra={
fields.length > 1 ? (
<Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(field.name)}>
</Button>
) : null
}
>
<div className="agent-editor-tool-grid">
<Form.Item
label="name工具名"
name={[field.name, 'name']}
rules={[
{ required: true, message: '请输入工具名' },
{ pattern: /^[A-Za-z0-9_]+$/, message: '仅支持字母、数字和下划线' },
]}
>
<Input placeholder="query_hot_selling_products" />
</Form.Item>
<Form.Item label="method调用方法" name={[field.name, 'method']} rules={[{ required: true }]}>
<Select options={['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map((value) => ({ value, label: value }))} />
</Form.Item>
</div>
<Form.Item
label="description描述"
name={[field.name, 'description']}
rules={[{ required: true, message: '请输入描述' }]}
>
<Input.TextArea rows={2} placeholder="描述调用时机和工具能力" />
</Form.Item>
<Form.Item label="pathAPI 地址)" name={[field.name, 'path']} rules={[{ required: true, message: '请输入 API 地址' }]}>
<Input placeholder="/v1/products/hot-selling" />
</Form.Item>
<div className="agent-editor-tool-grid">
<Form.Item label="headers请求头 JSON" name={[field.name, 'headers']}>
<Input.TextArea rows={7} className="agent-editor-code-input" placeholder={'{\n "X-Custom-Source": "aura-agent"\n}'} />
</Form.Item>
<Form.Item
label="parametersSchema依赖参数 JSON Schema"
name={[field.name, 'parametersSchema']}
rules={[{ required: true, message: '请输入依赖参数 Schema' }]}
>
<Input.TextArea rows={7} className="agent-editor-code-input" />
</Form.Item>
</div>
</Card>
))}
</Space>
)}
</Form.List>
</Form>
</Modal>
);
}

View File

@ -17,6 +17,12 @@ interface CapabilitySettingsProps {
setAvatarSelectorOpen: (open: boolean) => void; setAvatarSelectorOpen: (open: boolean) => void;
beforeUploadKnowledge: (file: any) => Promise<boolean>; beforeUploadKnowledge: (file: any) => Promise<boolean>;
onDeleteKnowledge: (fileId: string) => Promise<void>; onDeleteKnowledge: (fileId: string) => Promise<void>;
onCreateSkill: () => void;
onEditSkill: (skillId: string) => void;
onDeleteSkill: (skillId: string) => Promise<void>;
onCreateExternalTool: () => void;
onEditExternalTool: (pluginId: string) => void;
onDeleteExternalTool: (pluginId: string) => Promise<void>;
markDirty: () => void; markDirty: () => void;
isMobile?: boolean; isMobile?: boolean;
} }
@ -32,6 +38,12 @@ export default function CapabilitySettings({
setAvatarSelectorOpen, setAvatarSelectorOpen,
beforeUploadKnowledge, beforeUploadKnowledge,
onDeleteKnowledge, onDeleteKnowledge,
onCreateSkill,
onEditSkill,
onDeleteSkill,
onCreateExternalTool,
onEditExternalTool,
onDeleteExternalTool,
markDirty, markDirty,
isMobile = false, isMobile = false,
}: CapabilitySettingsProps) { }: CapabilitySettingsProps) {
@ -62,6 +74,12 @@ export default function CapabilitySettings({
agent={agent} agent={agent}
beforeUploadKnowledge={beforeUploadKnowledge} beforeUploadKnowledge={beforeUploadKnowledge}
onDeleteKnowledge={onDeleteKnowledge} onDeleteKnowledge={onDeleteKnowledge}
onCreateSkill={onCreateSkill}
onEditSkill={onEditSkill}
onDeleteSkill={onDeleteSkill}
onCreateExternalTool={onCreateExternalTool}
onEditExternalTool={onEditExternalTool}
onDeleteExternalTool={onDeleteExternalTool}
/> />
<WebSearchCard /> <WebSearchCard />
</Form> </Form>

View File

@ -1,5 +1,5 @@
import { Button, Collapse, Form, Input, List, Popconfirm, Tag } from 'antd'; import { ApiOutlined, DatabaseOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ToolOutlined } from '@ant-design/icons';
import { DatabaseOutlined, ToolOutlined } from '@ant-design/icons'; import { Button, Card, Collapse, Input, List, Popconfirm, Space, Tag } from 'antd';
import { Agent } from '../../../../api'; import { Agent } from '../../../../api';
import { STATUS_TAG } from '../../constants'; import { STATUS_TAG } from '../../constants';
@ -7,13 +7,28 @@ interface KnowledgeSettingsPanelProps {
agent: Agent | null; agent: Agent | null;
beforeUploadKnowledge: (file: any) => Promise<boolean>; beforeUploadKnowledge: (file: any) => Promise<boolean>;
onDeleteKnowledge: (fileId: string) => Promise<void>; onDeleteKnowledge: (fileId: string) => Promise<void>;
onCreateSkill: () => void;
onEditSkill: (skillId: string) => void;
onDeleteSkill: (skillId: string) => Promise<void>;
onCreateExternalTool: () => void;
onEditExternalTool: (pluginId: string) => void;
onDeleteExternalTool: (pluginId: string) => Promise<void>;
} }
export default function KnowledgeSettingsPanel({ export default function KnowledgeSettingsPanel({
agent, agent,
beforeUploadKnowledge, beforeUploadKnowledge,
onDeleteKnowledge, onDeleteKnowledge,
onCreateSkill,
onEditSkill,
onDeleteSkill,
onCreateExternalTool,
onEditExternalTool,
onDeleteExternalTool,
}: KnowledgeSettingsPanelProps) { }: KnowledgeSettingsPanelProps) {
const skills = agent?.skills ?? [];
const plugins = agent?.plugins ?? [];
return ( return (
<Collapse <Collapse
ghost ghost
@ -37,12 +52,13 @@ export default function KnowledgeSettingsPanel({
multiple multiple
className="agent-editor-file-input" className="agent-editor-file-input"
id="knowledge-upload" id="knowledge-upload"
onChange={async (e) => { onChange={async (event) => {
const files = e.target.files; const files = event.target.files;
if (!files) return; if (!files) return;
for (let i = 0; i < files.length; i++) { for (let index = 0; index < files.length; index++) {
await beforeUploadKnowledge(files[i]); await beforeUploadKnowledge(files[index]);
} }
event.target.value = '';
}} }}
/> />
<Button <Button
@ -79,8 +95,8 @@ export default function KnowledgeSettingsPanel({
<span className="agent-editor-indexing-label"></span> <span className="agent-editor-indexing-label"></span>
</Tag> </Tag>
) : ( ) : (
<Tag color={STATUS_TAG[(item.status || 'ready')].color} className="m-0 text-[10px] px-1"> <Tag color={STATUS_TAG[item.status || 'ready'].color} className="m-0 text-[10px] px-1">
{STATUS_TAG[(item.status || 'ready')].text} {STATUS_TAG[item.status || 'ready'].text}
</Tag> </Tag>
)} )}
</span> </span>
@ -93,14 +109,144 @@ export default function KnowledgeSettingsPanel({
}, },
{ {
key: 'skills', key: 'skills',
collapsible: 'disabled',
label: ( label: (
<div className="agent-editor-disabled-label" title="技能功能开发中"> <div className="agent-editor-collapse-label">
<ToolOutlined /> <ToolOutlined className="agent-editor-label-icon" />
& () & ({skills.length + plugins.length})
</div>
),
children: (
<div className="px-1">
<div className="agent-editor-skill-actions">
<span className="text-xs text-gray-500">Markdown API </span>
<Space wrap>
<Button size="small" icon={<PlusOutlined />} onClick={onCreateSkill}>
Skill
</Button>
<Button type="primary" ghost size="small" icon={<ApiOutlined />} onClick={onCreateExternalTool}>
</Button>
</Space>
</div>
<div className="agent-editor-tool-section">
<div className="agent-editor-tool-section-title">
<span>Skills</span>
<Tag>{skills.length}</Tag>
</div>
<List
size="small"
locale={{ emptyText: '暂无 Markdown Skill' }}
dataSource={skills}
renderItem={(item) => (
<List.Item
className="agent-editor-skill-item"
actions={[
<Button key="edit" type="text" size="small" icon={<EditOutlined />} onClick={() => onEditSkill(item.id)}>
</Button>,
<Popconfirm key="delete" title="确认删除该 Skill" onConfirm={() => onDeleteSkill(item.id)}>
<Button type="text" danger size="small" icon={<DeleteOutlined />}>
</Button>
</Popconfirm>,
]}
>
<List.Item.Meta
title={<span className="agent-editor-skill-name">{item.filename || item.name}</span>}
description={
<div>
<div>{item.description || '暂无描述'}</div>
{item.filename && item.filename !== item.name && <div className="agent-editor-tool-help">{item.name}</div>}
</div>
}
/>
<Space size={6} wrap>
<Tag color="blue">{item.type}</Tag>
<Tag color={item.enabled ? 'success' : 'default'}>{item.enabled ? '已启用' : '未启用'}</Tag>
</Space>
</List.Item>
)}
/>
</div>
<div className="agent-editor-tool-section">
<div className="agent-editor-tool-section-title">
<span></span>
<Tag>{plugins.length}</Tag>
</div>
{plugins.length === 0 ? (
<div className="agent-editor-tool-empty"></div>
) : (
<Space direction="vertical" size={10} className="agent-editor-tool-list">
{plugins.map((plugin) => (
<Card
key={plugin.id}
size="small"
className="agent-editor-plugin-card"
title={
<Space wrap>
<ApiOutlined className="agent-editor-label-icon" />
<span>{plugin.name}</span>
<Tag color={plugin.enabled ? 'success' : 'default'}>{plugin.enabled ? '已启用' : '未启用'}</Tag>
</Space>
}
extra={
<Space>
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => onEditExternalTool(plugin.id)}>
</Button>
<Popconfirm title="确认删除该工具集及其全部 API" onConfirm={() => onDeleteExternalTool(plugin.id)}>
<Button type="text" danger size="small" icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
}
>
<div className="agent-editor-plugin-description">{plugin.description || '暂无描述'}</div>
<Space size={6} wrap className="agent-editor-plugin-meta">
<Tag>{plugin.authType || 'none'}</Tag>
<span>{plugin.baseUrl}</span>
<span>{plugin.apis.length} API</span>
</Space>
<Collapse
ghost
size="small"
className="agent-editor-plugin-apis"
items={plugin.apis.map((api) => ({
key: api.id || api.name,
label: (
<Space wrap>
<Tag color="geekblue">{api.method}</Tag>
<strong>{api.name}</strong>
<span className="agent-editor-tool-help">{api.path}</span>
</Space>
),
children: (
<div className="agent-editor-api-detail">
<div>{api.description || '暂无描述'}</div>
<div className="agent-editor-api-config-grid">
<div>
<strong>headers</strong>
<pre>{JSON.stringify(api.headers || {}, null, 2)}</pre>
</div>
<div>
<strong>parametersSchema</strong>
<pre>{JSON.stringify(api.parametersSchema || {}, null, 2)}</pre>
</div>
</div>
</div>
),
}))}
/>
</Card>
))}
</Space>
)}
</div>
</div> </div>
), ),
children: null,
}, },
]} ]}
/> />

View File

@ -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) => { const beforeUploadKnowledge = async (file: any) => {
if (!id) { if (!id) {
message.warning('请先保存智能体基础信息后再上传'); message.warning('请先保存智能体基础信息后再上传');
@ -328,6 +350,8 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE
beforeUploadEditAvatar, beforeUploadEditAvatar,
handleAvatarSelect, handleAvatarSelect,
handleDeleteKnowledge, handleDeleteKnowledge,
handleDeleteSkill,
handleDeletePlugin,
liveAgent, liveAgent,
currentName, currentName,
markDirty, markDirty,

View File

@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { App as AntApp } from 'antd'; import { App as AntApp } from 'antd';
import { FileTextOutlined, SaveOutlined } from '@ant-design/icons'; import { FileTextOutlined, SaveOutlined } from '@ant-design/icons';
import SkillEditor from '../../components/SkillEditor'; import SkillEditor from '../../components/SkillEditor';
import ExternalToolEditor from '../../components/ExternalToolEditor';
import { useAgentEditor } from './hooks/useAgentEditor'; import { useAgentEditor } from './hooks/useAgentEditor';
import { useIsMobile } from '../../hooks/useIsMobile'; import { useIsMobile } from '../../hooks/useIsMobile';
import Header from './components/Header'; import Header from './components/Header';
@ -24,6 +25,8 @@ export default function AgentEditor() {
const navigate = useNavigate(); const navigate = useNavigate();
const { message } = AntApp.useApp(); const { message } = AntApp.useApp();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [externalToolEditorOpen, setExternalToolEditorOpen] = useState(false);
const [editingPluginId, setEditingPluginId] = useState<string | null>(null);
const { const {
agent, agent,
@ -52,6 +55,8 @@ export default function AgentEditor() {
beforeUploadEditAvatar, beforeUploadEditAvatar,
handleAvatarSelect, handleAvatarSelect,
handleDeleteKnowledge, handleDeleteKnowledge,
handleDeleteSkill,
handleDeletePlugin,
liveAgent, liveAgent,
currentName, currentName,
markDirty, markDirty,
@ -85,6 +90,24 @@ export default function AgentEditor() {
setAvatarSelectorOpen={setAvatarSelectorOpen} setAvatarSelectorOpen={setAvatarSelectorOpen}
beforeUploadKnowledge={beforeUploadKnowledge} beforeUploadKnowledge={beforeUploadKnowledge}
onDeleteKnowledge={handleDeleteKnowledge} 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} markDirty={markDirty}
isMobile={isMobile} isMobile={isMobile}
/> />
@ -120,13 +143,28 @@ export default function AgentEditor() {
)} )}
{!isNew && ( {!isNew && (
<>
<SkillEditor <SkillEditor
open={skillEditorOpen} open={skillEditorOpen}
agentId={id!} agentId={id!}
skillId={editingSkillId} skillId={editingSkillId}
onClose={() => setSkillEditorOpen(false)} onClose={() => {
setSkillEditorOpen(false);
setEditingSkillId(null);
}}
onSaved={refresh} onSaved={refresh}
/> />
<ExternalToolEditor
open={externalToolEditorOpen}
agentId={id!}
plugin={agent?.plugins?.find((plugin) => plugin.id === editingPluginId)}
onClose={() => {
setExternalToolEditorOpen(false);
setEditingPluginId(null);
}}
onSaved={refresh}
/>
</>
)} )}
</div> </div>

View File

@ -72,6 +72,138 @@
color: var(--color-brand); 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-field-input,
.agent-editor-number-input { .agent-editor-number-input {
height: 2.625rem; height: 2.625rem;