优化外部工具 API 卡片折叠交互

main
sp mac bookpro 2605 2026-07-23 21:22:36 +08:00
parent be0dae0719
commit e33a1c3741
2 changed files with 193 additions and 136 deletions

View File

@ -9,5 +9,6 @@ alwaysApply: true
3. **代码修改** - 任何时候当存在字段格式不对变量名不对表使用不对等禁止做兼容修改必须按唯一性修改。比如约定字段是string正确只能传string错误可以传int。 比如约定字段名是data正确只能传data错误可以传sourceData或者data。 3. **代码修改** - 任何时候当存在字段格式不对变量名不对表使用不对等禁止做兼容修改必须按唯一性修改。比如约定字段是string正确只能传string错误可以传int。 比如约定字段名是data正确只能传data错误可以传sourceData或者data。
4. **语言** - 永远使用中文跟用户沟通,专有名词可以用英文。 4. **语言** - 永远使用中文跟用户沟通,专有名词可以用英文。
5. **文档** - 在用户没有明确要求先提供文档时,禁止创建文档。避免生产垃圾文件。用户明确要求提供文档时,才创建文档。 5. **文档** - 在用户没有明确要求先提供文档时,禁止创建文档。避免生产垃圾文件。用户明确要求提供文档时,才创建文档。
6. **注释** - 你生成的代码尽可能完善中文注释。注释的格式需要按照Go语言的注释规范。要描述清楚代码的功能参数返回值异常等。

View File

@ -1,5 +1,6 @@
import { CopyOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { CopyOutlined, DownOutlined, MinusCircleOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
import { App as AntApp, Button, Card, Form, Input, Modal, Select, Space } from 'antd'; import { App as AntApp, Button, Card, Form, Input, Modal, Select, Space } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { AgentAPI, ExternalToolApi, ExternalToolApiRouting, ExternalToolPlugin, ExternalToolPluginPayload } from '../api'; import { AgentAPI, ExternalToolApi, ExternalToolApiRouting, ExternalToolPlugin, ExternalToolPluginPayload } from '../api';
interface Props { interface Props {
@ -137,6 +138,43 @@ export default function ExternalToolEditor({ open, agentId, plugin, onClose, onS
const { message } = AntApp.useApp(); const { message } = AntApp.useApp();
const [form] = Form.useForm<ToolPluginFormValue>(); const [form] = Form.useForm<ToolPluginFormValue>();
const isEditing = Boolean(plugin); const isEditing = Boolean(plugin);
const apis = Form.useWatch('apis', form) || [];
const [expandedApiIndexes, setExpandedApiIndexes] = useState<number[]>([]);
const previousApiCountRef = useRef(0);
useEffect(() => {
if (!open) {
previousApiCountRef.current = 0;
setExpandedApiIndexes([]);
return;
}
const apiCount = apis.length;
if (apiCount < 1) {
previousApiCountRef.current = 0;
setExpandedApiIndexes([]);
return;
}
const previousApiCount = previousApiCountRef.current;
if (previousApiCount === 0) {
setExpandedApiIndexes(apiCount === 1 ? [0] : []);
} else if (previousApiCount === 1 && apiCount > 1) {
setExpandedApiIndexes([]);
} else if (apiCount === 1) {
setExpandedApiIndexes([0]);
} else {
setExpandedApiIndexes((current) => current.filter((index) => index < apiCount));
}
previousApiCountRef.current = apiCount;
}, [apis.length, open]);
const toggleApiCard = (index: number) => {
setExpandedApiIndexes((current) =>
current.includes(index) ? current.filter((item) => item !== index) : [...current, index],
);
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@ -264,149 +302,167 @@ export default function ExternalToolEditor({ open, agentId, plugin, onClose, onS
</Button> </Button>
</div> </div>
{fields.map((field, index) => ( {fields.map((field, index) => {
<Card const isExpanded = expandedApiIndexes.includes(index);
key={field.key}
size="small" return (
title={`API ${index + 1}`} <Card
className="agent-editor-tool-card" key={field.key}
extra={ size="small"
<Space> title={
<Button <Button
type="text" type="text"
size="small" size="small"
icon={<CopyOutlined />} onClick={() => toggleApiCard(index)}
onClick={() => { style={{ padding: 0, fontWeight: 500 }}
const currentApis = form.getFieldValue('apis') || []; icon={isExpanded ? <DownOutlined /> : <RightOutlined />}
const apiToCopy = currentApis[field.name];
if (apiToCopy) {
add({
...apiToCopy,
name: `${apiToCopy.name}-copy`,
});
}
}}
> >
{`API ${index + 1}`}
</Button> </Button>
{fields.length > 1 ? ( }
<Button type="text" danger size="small" icon={<MinusCircleOutlined />} onClick={() => remove(field.name)}> className="agent-editor-tool-card"
extra={
<Space>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => {
const currentApis = form.getFieldValue('apis') || [];
const apiToCopy = currentApis[field.name];
if (apiToCopy) {
add({
...apiToCopy,
name: `${apiToCopy.name}-copy`,
});
}
}}
>
</Button> </Button>
) : null} {fields.length > 1 ? (
</Space> <Button type="text" danger size="small" icon={<MinusCircleOutlined />} onClick={() => remove(field.name)}>
}
> </Button>
<div className="agent-editor-tool-grid"> ) : null}
<Form.Item </Space>
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={[{ validator: validateTrimmedText('请输入描述') }]}
> >
<Input.TextArea rows={2} placeholder="描述调用时机和工具能力" /> {isExpanded ? (
</Form.Item> <>
<Form.Item <div className="agent-editor-tool-grid">
label="pathAPI 地址)" <Form.Item
name={[field.name, 'path']} label="name工具名"
rules={[{ validator: validateTrimmedText('请输入 API 地址') }]} name={[field.name, 'name']}
> rules={[
<Input placeholder="/v1/products/hot-selling" /> { required: true, message: '请输入工具名' },
</Form.Item> { pattern: /^[A-Za-z0-9_]+$/, message: '仅支持字母、数字和下划线' },
<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}'} /> <Input placeholder="query_hot_selling_products" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label="method调用方法" name={[field.name, 'method']} rules={[{ required: true }]}>
label="parametersSchema依赖参数 JSON 或对象)" <Select options={['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map((value) => ({ value, label: value }))} />
name={[field.name, 'parametersSchema']} </Form.Item>
rules={[{ required: true, message: '请输入依赖参数 Schema' }]} </div>
> <Form.Item
<Input.TextArea rows={7} className="agent-editor-code-input" /> label="description描述"
</Form.Item> name={[field.name, 'description']}
</div> rules={[{ validator: validateTrimmedText('请输入描述') }]}
<Card size="small" title="routing路由规则"> >
<Form.Item label="summary" name={[field.name, 'routing', 'summary']}> <Input.TextArea rows={2} placeholder="描述调用时机和工具能力" />
<Input placeholder="查询商品维度数据" /> </Form.Item>
</Form.Item> <Form.Item
<Form.List name={[field.name, 'routing', 'useWhen']} rules={[{ validator: validateRoutingList('至少添加一条 useWhen', 1) }]}> label="pathAPI 地址)"
{(routingFields, { add: addUseWhen, remove: removeUseWhen }, { errors }) => ( name={[field.name, 'path']}
<Space direction="vertical" size={8} className="agent-editor-tool-list"> rules={[{ validator: validateTrimmedText('请输入 API 地址') }]}
<div className="agent-editor-tool-list-header"> >
<div> <Input placeholder="/v1/products/hot-selling" />
<strong>useWhen</strong> </Form.Item>
<div className="agent-editor-tool-help"> API</div> <div className="agent-editor-tool-grid">
</div> <Form.Item label="headers请求头 JSON 或对象)" name={[field.name, 'headers']}>
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addUseWhen('')}> <Input.TextArea rows={7} className="agent-editor-code-input" placeholder={'{\n "X-Custom-Source": "aura-agent"\n}'} />
</Form.Item>
</Button> <Form.Item
</div> label="parametersSchema依赖参数 JSON 或对象)"
{routingFields.map((routingField) => ( name={[field.name, 'parametersSchema']}
<Space key={routingField.key} align="start" className="agent-editor-tool-list"> rules={[{ required: true, message: '请输入依赖参数 Schema' }]}
<Form.Item >
name={routingField.name} <Input.TextArea rows={7} className="agent-editor-code-input" />
className="flex-1 mb-0" </Form.Item>
rules={[{ validator: validateTrimmedText('条件不能为空') }]} </div>
> <Card size="small" title="routing路由规则">
<Input placeholder="用户明确要求商品数据" style={{ width: 600 }} /> <Form.Item label="summary" name={[field.name, 'routing', 'summary']}>
</Form.Item> <Input placeholder="查询商品维度数据" />
<Button </Form.Item>
danger <Form.List name={[field.name, 'routing', 'useWhen']} rules={[{ validator: validateRoutingList('至少添加一条 useWhen', 1) }]}>
type="text" {(routingFields, { add: addUseWhen, remove: removeUseWhen }, { errors }) => (
icon={<MinusCircleOutlined />} <Space direction="vertical" size={8} className="agent-editor-tool-list">
onClick={() => removeUseWhen(routingField.name)} <div className="agent-editor-tool-list-header">
disabled={routingFields.length <= 1} <div>
/> <strong>useWhen</strong>
</Space> <div className="agent-editor-tool-help"> API</div>
))} </div>
<Form.ErrorList errors={errors} /> <Button type="dashed" icon={<PlusOutlined />} onClick={() => addUseWhen('')}>
</Space>
)} </Button>
</Form.List> </div>
<Form.List name={[field.name, 'routing', 'doNotUseWhen']} rules={[{ validator: validateRoutingList('列表项不能为空') }]}> {routingFields.map((routingField) => (
{(routingFields, { add: addDoNotUseWhen, remove: removeDoNotUseWhen }, { errors }) => ( <Space key={routingField.key} align="start" className="agent-editor-tool-list">
<Space direction="vertical" size={8} className="agent-editor-tool-list" style={{ marginTop: 16 }}> <Form.Item
<div className="agent-editor-tool-list-header"> name={routingField.name}
<div> className="flex-1 mb-0"
<strong>doNotUseWhen</strong> rules={[{ validator: validateTrimmedText('条件不能为空') }]}
<div className="agent-editor-tool-help"> API</div> >
</div> <Input placeholder="用户明确要求商品数据" style={{ width: 600 }} />
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addDoNotUseWhen('')}> </Form.Item>
<Button
</Button> danger
</div> type="text"
{routingFields.map((routingField) => ( icon={<MinusCircleOutlined />}
<Space key={routingField.key} align="start" className="agent-editor-tool-list"> onClick={() => removeUseWhen(routingField.name)}
<Form.Item disabled={routingFields.length <= 1}
name={routingField.name} />
className="flex-1 mb-0" </Space>
rules={[{ validator: validateTrimmedText('条件不能为空') }]} ))}
> <Form.ErrorList errors={errors} />
<Input placeholder="当前问题只需要其他维度数据" style={{ width: 600 }} /> </Space>
</Form.Item> )}
<Button danger type="text" icon={<MinusCircleOutlined />} onClick={() => removeDoNotUseWhen(routingField.name)} /> </Form.List>
</Space> <Form.List name={[field.name, 'routing', 'doNotUseWhen']} rules={[{ validator: validateRoutingList('列表项不能为空') }]}>
))} {(routingFields, { add: addDoNotUseWhen, remove: removeDoNotUseWhen }, { errors }) => (
<Form.ErrorList errors={errors} /> <Space direction="vertical" size={8} className="agent-editor-tool-list" style={{ marginTop: 16 }}>
</Space> <div className="agent-editor-tool-list-header">
)} <div>
</Form.List> <strong>doNotUseWhen</strong>
<div className="agent-editor-tool-help"> API</div>
</div>
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addDoNotUseWhen('')}>
</Button>
</div>
{routingFields.map((routingField) => (
<Space key={routingField.key} align="start" className="agent-editor-tool-list">
<Form.Item
name={routingField.name}
className="flex-1 mb-0"
rules={[{ validator: validateTrimmedText('条件不能为空') }]}
>
<Input placeholder="当前问题只需要其他维度数据" style={{ width: 600 }} />
</Form.Item>
<Button danger type="text" icon={<MinusCircleOutlined />} onClick={() => removeDoNotUseWhen(routingField.name)} />
</Space>
))}
<Form.ErrorList errors={errors} />
</Space>
)}
</Form.List>
</Card>
</>
) : null}
</Card> </Card>
</Card> );
))} })}
</Space> </Space>
)} )}
</Form.List> </Form.List>