fix: 修复 JSON 编辑器同步时对象字段类型错误,并拆分模块控制文件行数
parent
49b48df182
commit
67e69d4d3b
|
|
@ -10,5 +10,6 @@ alwaysApply: true
|
||||||
4. **语言** - 永远使用中文跟用户沟通,专有名词可以用英文。
|
4. **语言** - 永远使用中文跟用户沟通,专有名词可以用英文。
|
||||||
5. **文档** - 在用户没有明确要求先提供文档时,禁止创建文档。避免生产垃圾文件。用户明确要求提供文档时,才创建文档。
|
5. **文档** - 在用户没有明确要求先提供文档时,禁止创建文档。避免生产垃圾文件。用户明确要求提供文档时,才创建文档。
|
||||||
6. **注释** - 你生成的代码,尽可能完善中文注释。注释的格式需要按照Go语言的注释规范。要描述清楚代码的功能,参数,返回值,异常等。
|
6. **注释** - 你生成的代码,尽可能完善中文注释。注释的格式需要按照Go语言的注释规范。要描述清楚代码的功能,参数,返回值,异常等。
|
||||||
|
7. **代码格式** - .go文件,.ts, .tsx 文件,单文件代码不超过300行,当超过300行时,需要做拆分,按功能模块拆分,同模块在同一个文件夹下,文件夹名要语义化。
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export interface ExternalToolPluginPayload {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
authType: 'none' | 'bearer' | 'basic' | 'apiKey';
|
authType: 'none' | 'bearer' | 'basic' | 'apiKey' | 'custom';
|
||||||
authConfig: Record<string, unknown>;
|
authConfig: Record<string, unknown>;
|
||||||
headers?: Record<string, string> | null;
|
headers?: Record<string, string> | null;
|
||||||
apis: ExternalToolApi[];
|
apis: ExternalToolApi[];
|
||||||
|
|
|
||||||
|
|
@ -1,603 +0,0 @@
|
||||||
import { CopyOutlined, DownOutlined, MinusCircleOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
|
|
||||||
import { App as AntApp, Button, Card, Form, Input, Modal, Select, Space, Tabs } from 'antd';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { AgentAPI, ExternalToolApi, ExternalToolApiRouting, ExternalToolPlugin, ExternalToolPluginPayload } from '../api';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: boolean;
|
|
||||||
agentId: string;
|
|
||||||
plugin?: ExternalToolPlugin | null;
|
|
||||||
onClose: () => void;
|
|
||||||
onSaved?: () => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToolApiRoutingFormValue extends ExternalToolApiRouting {}
|
|
||||||
|
|
||||||
interface ToolApiFormValue extends Omit<ExternalToolApi, 'headers' | 'parametersSchema' | 'routing'> {
|
|
||||||
headers?: string;
|
|
||||||
parametersSchema: string;
|
|
||||||
routing: ToolApiRoutingFormValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToolPluginFormValue extends Omit<ExternalToolPluginPayload, 'authConfig' | 'apis' | 'headers'> {
|
|
||||||
authConfig?: string;
|
|
||||||
headers?: string;
|
|
||||||
apis: ToolApiFormValue[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMPTY_API: ToolApiFormValue = {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
method: 'GET',
|
|
||||||
path: '',
|
|
||||||
headers: '{}',
|
|
||||||
parametersSchema: JSON.stringify({ type: 'object', properties: {} }, null, 2),
|
|
||||||
routing: {
|
|
||||||
summary: '',
|
|
||||||
useWhen: [''],
|
|
||||||
doNotUseWhen: [],
|
|
||||||
domains: [],
|
|
||||||
intents: [],
|
|
||||||
requiredSlots: [],
|
|
||||||
optionalSlots: [],
|
|
||||||
examples: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseJsonObject(value: string | undefined, fieldName: string, optional = false) {
|
|
||||||
const trimmedValue = value?.trim();
|
|
||||||
if (!trimmedValue) {
|
|
||||||
if (optional) return undefined;
|
|
||||||
throw new Error(`${fieldName}不能为空`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 优先尝试标准 JSON 解析
|
|
||||||
return JSON.parse(trimmedValue);
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
// 2. 失败后尝试作为 JS 对象解析 (支持无引号键、单引号等)
|
|
||||||
// eslint-disable-next-line no-new-func
|
|
||||||
const parsed = new Function(`return (${trimmedValue})`)();
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
throw new Error();
|
|
||||||
} catch (e2) {
|
|
||||||
// 3. 尝试宽松解析 (处理类似 { Content-Type: application/json } 这种完全无引号的情况)
|
|
||||||
try {
|
|
||||||
const content = trimmedValue.replace(/^\{/, '').replace(/\}$/, '').trim();
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
const pairs = content.split(/[\n,]/);
|
|
||||||
let hasValidPair = false;
|
|
||||||
|
|
||||||
for (let pair of pairs) {
|
|
||||||
pair = pair.trim();
|
|
||||||
if (!pair) continue;
|
|
||||||
const colonIndex = pair.indexOf(':');
|
|
||||||
if (colonIndex > 0) {
|
|
||||||
const k = pair.substring(0, colonIndex).trim().replace(/^['"]|['"]$/g, '');
|
|
||||||
const v = pair.substring(colonIndex + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
||||||
if (k) {
|
|
||||||
let finalVal: any = v;
|
|
||||||
if (v === 'true') finalVal = true;
|
|
||||||
else if (v === 'false') finalVal = false;
|
|
||||||
else if (v === 'null') finalVal = null;
|
|
||||||
else if (!isNaN(Number(v)) && v !== '') finalVal = Number(v);
|
|
||||||
result[k] = finalVal;
|
|
||||||
hasValidPair = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasValidPair) return result;
|
|
||||||
} catch (e3) {
|
|
||||||
// ignore and fall through to error
|
|
||||||
}
|
|
||||||
throw new Error(`${fieldName}格式不正确,请确保是有效的 JSON 或对象格式`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeStringList(values?: string[]) {
|
|
||||||
return Array.from(new Set((values ?? []).map((item) => item?.trim()).filter((item): item is string => Boolean(item))));
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateTrimmedText(message: string) {
|
|
||||||
return async (_: unknown, value: string | undefined) => {
|
|
||||||
if (!value?.trim()) {
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateRoutingList(message: string, min = 0) {
|
|
||||||
return async (_: unknown, value: string[] | undefined) => {
|
|
||||||
const normalized = normalizeStringList(value);
|
|
||||||
if (normalized.length < min) {
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((value ?? []).some((item) => !item?.trim())) {
|
|
||||||
throw new Error('列表项不能为空');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRouting(value: ToolApiRoutingFormValue | undefined, apiName: string): ExternalToolApiRouting {
|
|
||||||
if (!value) {
|
|
||||||
throw new Error(`API ${apiName} 的路由规则不能为空`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const useWhen = normalizeStringList(value.useWhen);
|
|
||||||
if (useWhen.length < 1) {
|
|
||||||
throw new Error(`API ${apiName} 的 useWhen 至少保留一项`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
summary: value.summary?.trim() || '',
|
|
||||||
useWhen,
|
|
||||||
doNotUseWhen: normalizeStringList(value.doNotUseWhen),
|
|
||||||
domains: normalizeStringList(value.domains),
|
|
||||||
intents: normalizeStringList(value.intents),
|
|
||||||
requiredSlots: normalizeStringList(value.requiredSlots),
|
|
||||||
optionalSlots: normalizeStringList(value.optionalSlots),
|
|
||||||
examples: normalizeStringList(value.examples),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ExternalToolEditor({ open, agentId, plugin, onClose, onSaved }: Props) {
|
|
||||||
const { message } = AntApp.useApp();
|
|
||||||
const [form] = Form.useForm<ToolPluginFormValue>();
|
|
||||||
const isEditing = Boolean(plugin);
|
|
||||||
const apis = Form.useWatch('apis', form) || [];
|
|
||||||
const allValues = Form.useWatch([], form);
|
|
||||||
const [expandedApiIndexes, setExpandedApiIndexes] = useState<number[]>([]);
|
|
||||||
const [jsonContent, setJsonContent] = useState('');
|
|
||||||
const [activeTab, setActiveTab] = useState('visual');
|
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
|
||||||
const previousApiCountRef = useRef(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (allValues && !isSyncing) {
|
|
||||||
setJsonContent(JSON.stringify(allValues, null, 2));
|
|
||||||
}
|
|
||||||
}, [allValues, isSyncing]);
|
|
||||||
|
|
||||||
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 handleJsonChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
setJsonContent(val);
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(val);
|
|
||||||
if (parsed && typeof parsed === 'object') {
|
|
||||||
setIsSyncing(true);
|
|
||||||
form.setFieldsValue(parsed);
|
|
||||||
setTimeout(() => setIsSyncing(false), 0);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 格式不正确时不更新表单
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleApiCard = (index: number) => {
|
|
||||||
setExpandedApiIndexes((current) =>
|
|
||||||
current.includes(index) ? current.filter((item) => item !== index) : [...current, index],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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') || {},
|
|
||||||
headers: parseJsonObject(values.headers, '统一请求头', true),
|
|
||||||
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} 的依赖参数`),
|
|
||||||
routing: normalizeRouting(item.routing, item.name.trim() || '未命名 API'),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
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),
|
|
||||||
headers: JSON.stringify(plugin.headers || {}, 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),
|
|
||||||
routing: {
|
|
||||||
summary: item.routing?.summary || '',
|
|
||||||
useWhen: item.routing?.useWhen?.length ? item.routing.useWhen : [''],
|
|
||||||
doNotUseWhen: item.routing?.doNotUseWhen || [],
|
|
||||||
domains: item.routing?.domains || [],
|
|
||||||
intents: item.routing?.intents || [],
|
|
||||||
requiredSlots: item.routing?.requiredSlots || [],
|
|
||||||
optionalSlots: item.routing?.optionalSlots || [],
|
|
||||||
examples: item.routing?.examples || [],
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
baseUrl: '',
|
|
||||||
authType: 'bearer',
|
|
||||||
authConfig: JSON.stringify({ token: '' }, null, 2),
|
|
||||||
headers: JSON.stringify({}, null, 2),
|
|
||||||
apis: [{ ...EMPTY_API }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form form={form} layout="vertical" requiredMark="optional" preserve={true}>
|
|
||||||
<Tabs
|
|
||||||
activeKey={activeTab}
|
|
||||||
onChange={setActiveTab}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: 'visual',
|
|
||||||
label: '可视化编辑',
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
<Form.Item label="统一请求头(JSON 或对象)" name="headers">
|
|
||||||
<Input.TextArea
|
|
||||||
rows={3}
|
|
||||||
className="agent-editor-code-input"
|
|
||||||
placeholder={'{\n "Authorization": "Bearer YOUR_TOKEN"\n}'}
|
|
||||||
/>
|
|
||||||
</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) => {
|
|
||||||
const isExpanded = expandedApiIndexes.includes(index);
|
|
||||||
const apiName = apis[index]?.name;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={field.key}
|
|
||||||
size="small"
|
|
||||||
title={
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
onClick={() => toggleApiCard(index)}
|
|
||||||
style={{ padding: 0, fontWeight: 500 }}
|
|
||||||
icon={isExpanded ? <DownOutlined /> : <RightOutlined />}
|
|
||||||
>
|
|
||||||
{apiName || `API-${index}`}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
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>
|
|
||||||
{fields.length > 1 ? (
|
|
||||||
<Button type="text" danger size="small" icon={<MinusCircleOutlined />} onClick={() => remove(field.name)}>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isExpanded ? (
|
|
||||||
<>
|
|
||||||
<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={[{ validator: validateTrimmedText('请输入描述') }]}
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={2} placeholder="描述调用时机和工具能力" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="path(API 地址)"
|
|
||||||
name={[field.name, 'path']}
|
|
||||||
rules={[{ validator: validateTrimmedText('请输入 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 或对象)"
|
|
||||||
name={[field.name, 'parametersSchema']}
|
|
||||||
rules={[{ required: true, message: '请输入依赖参数 Schema' }]}
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={7} className="agent-editor-code-input" />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
<Card size="small" title="routing(路由规则)">
|
|
||||||
<Form.Item label="summary" name={[field.name, 'routing', 'summary']}>
|
|
||||||
<Input placeholder="查询商品维度数据" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.List name={[field.name, 'routing', 'useWhen']} rules={[{ validator: validateRoutingList('至少添加一条 useWhen', 1) }]}>
|
|
||||||
{(routingFields, { add: addUseWhen, remove: removeUseWhen }, { errors }) => (
|
|
||||||
<Space direction="vertical" size={8} className="agent-editor-tool-list">
|
|
||||||
<div className="agent-editor-tool-list-header">
|
|
||||||
<div>
|
|
||||||
<strong>useWhen</strong>
|
|
||||||
<div className="agent-editor-tool-help">至少一项,描述什么情况下应该调用这个 API。</div>
|
|
||||||
</div>
|
|
||||||
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addUseWhen('')}>
|
|
||||||
添加条件
|
|
||||||
</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={() => removeUseWhen(routingField.name)}
|
|
||||||
disabled={routingFields.length <= 1}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
))}
|
|
||||||
<Form.ErrorList errors={errors} />
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
<Form.List name={[field.name, 'routing', 'doNotUseWhen']} rules={[{ validator: validateRoutingList('列表项不能为空') }]}>
|
|
||||||
{(routingFields, { add: addDoNotUseWhen, remove: removeDoNotUseWhen }, { errors }) => (
|
|
||||||
<Space direction="vertical" size={8} className="agent-editor-tool-list" style={{ marginTop: 16 }}>
|
|
||||||
<div className="agent-editor-tool-list-header">
|
|
||||||
<div>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="agent-editor-tool-grid" style={{ marginTop: 16 }}>
|
|
||||||
<Form.Item label="domains(领域)" name={[field.name, 'routing', 'domains']}>
|
|
||||||
<Select mode="tags" placeholder="例如:e-commerce, logistics" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="intents(意图)" name={[field.name, 'routing', 'intents']}>
|
|
||||||
<Select mode="tags" placeholder="例如:query_order, cancel_order" />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="agent-editor-tool-grid">
|
|
||||||
<Form.Item label="requiredSlots(必填槽位)" name={[field.name, 'routing', 'requiredSlots']}>
|
|
||||||
<Select mode="tags" placeholder="例如:order_id, user_id" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="optionalSlots(可选槽位)" name={[field.name, 'routing', 'optionalSlots']}>
|
|
||||||
<Select mode="tags" placeholder="例如:start_date, end_date" />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form.List name={[field.name, 'routing', 'examples']}>
|
|
||||||
{(exampleFields, { add: addExample, remove: removeExample }) => (
|
|
||||||
<Space direction="vertical" size={8} className="agent-editor-tool-list" style={{ marginTop: 16 }}>
|
|
||||||
<div className="agent-editor-tool-list-header">
|
|
||||||
<div>
|
|
||||||
<strong>examples(示例)</strong>
|
|
||||||
<div className="agent-editor-tool-help">描述用户可能的提问方式。</div>
|
|
||||||
</div>
|
|
||||||
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addExample('')}>
|
|
||||||
添加示例
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{exampleFields.map((exampleField) => (
|
|
||||||
<Space key={exampleField.key} align="start" className="agent-editor-tool-list">
|
|
||||||
<Form.Item
|
|
||||||
name={exampleField.name}
|
|
||||||
className="flex-1 mb-0"
|
|
||||||
rules={[{ validator: validateTrimmedText('示例不能为空') }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="我想查一下最近的订单" style={{ width: 600 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Button danger type="text" icon={<MinusCircleOutlined />} onClick={() => removeExample(exampleField.name)} />
|
|
||||||
</Space>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'json',
|
|
||||||
label: 'JSON 编辑',
|
|
||||||
children: (
|
|
||||||
<div style={{ padding: '4px 0 16px' }}>
|
|
||||||
<div style={{ marginBottom: 8, color: '#666', fontSize: 12 }}>
|
|
||||||
提示:您可以直接编辑下方 JSON 内容,可视化表单将自动同步更新。
|
|
||||||
</div>
|
|
||||||
<Input.TextArea
|
|
||||||
value={jsonContent}
|
|
||||||
onChange={handleJsonChange}
|
|
||||||
rows={25}
|
|
||||||
className="agent-editor-code-input"
|
|
||||||
placeholder="请输入完整的工具集配置 JSON"
|
|
||||||
style={{ fontFamily: 'monospace' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Input } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface JsonEditorProps {
|
||||||
|
/** JSON 文本内容 */
|
||||||
|
value: string;
|
||||||
|
/** 内容变化回调 */
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 编辑区域。
|
||||||
|
* 用户可直接粘贴或编辑完整的工具集配置 JSON,修改会自动同步到可视化表单。
|
||||||
|
*/
|
||||||
|
export default function JsonEditor({ value, onChange }: JsonEditorProps) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '4px 0 16px' }}>
|
||||||
|
<div style={{ marginBottom: 8, color: '#666', fontSize: 12 }}>
|
||||||
|
提示:您可以直接编辑下方 JSON 内容,可视化表单将自动同步更新。
|
||||||
|
</div>
|
||||||
|
<Input.TextArea
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
rows={25}
|
||||||
|
className="agent-editor-code-input"
|
||||||
|
placeholder="请输入完整的工具集配置 JSON"
|
||||||
|
style={{ fontFamily: 'monospace' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
import { CopyOutlined, DownOutlined, MinusCircleOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Card, Form, Input, Select, Space } from 'antd';
|
||||||
|
import { ToolApiFormValue, ToolPluginFormValue, EMPTY_API } from './types';
|
||||||
|
import { validateTrimmedText, validateRoutingList } from './utils';
|
||||||
|
|
||||||
|
interface VisualEditorProps {
|
||||||
|
form: ReturnType<typeof Form.useForm<ToolPluginFormValue>>[0];
|
||||||
|
apis: ToolApiFormValue[];
|
||||||
|
expandedApiIndexes: number[];
|
||||||
|
onToggleApiCard: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可视化编辑区域。
|
||||||
|
* 包含工具集基础信息、统一请求头、认证配置,以及 API 功能列表(含路由规则)。
|
||||||
|
*/
|
||||||
|
export default function VisualEditor({ form, apis, expandedApiIndexes, onToggleApiCard }: VisualEditorProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
<Form.Item label="统一请求头(JSON 或对象)" name="headers">
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
className="agent-editor-code-input"
|
||||||
|
placeholder={'{\n "Authorization": "Bearer YOUR_TOKEN"\n}'}
|
||||||
|
/>
|
||||||
|
</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' },
|
||||||
|
{ value: 'custom', label: '自定义' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</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) => {
|
||||||
|
const isExpanded = expandedApiIndexes.includes(index);
|
||||||
|
const apiName = apis[index]?.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={field.key}
|
||||||
|
size="small"
|
||||||
|
title={
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={() => onToggleApiCard(index)}
|
||||||
|
style={{ padding: 0, fontWeight: 500 }}
|
||||||
|
icon={isExpanded ? <DownOutlined /> : <RightOutlined />}
|
||||||
|
>
|
||||||
|
{apiName || `API-${index}`}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
{fields.length > 1 ? (
|
||||||
|
<Button type="text" danger size="small" icon={<MinusCircleOutlined />} onClick={() => remove(field.name)}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<>
|
||||||
|
<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={[{ validator: validateTrimmedText('请输入描述') }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={2} placeholder="描述调用时机和工具能力" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="path(API 地址)"
|
||||||
|
name={[field.name, 'path']}
|
||||||
|
rules={[{ validator: validateTrimmedText('请输入 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 或对象)"
|
||||||
|
name={[field.name, 'parametersSchema']}
|
||||||
|
rules={[{ required: true, message: '请输入依赖参数 Schema' }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={7} className="agent-editor-code-input" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Card size="small" title="routing(路由规则)">
|
||||||
|
<Form.Item label="summary" name={[field.name, 'routing', 'summary']}>
|
||||||
|
<Input placeholder="查询商品维度数据" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.List name={[field.name, 'routing', 'useWhen']} rules={[{ validator: validateRoutingList('至少添加一条 useWhen', 1) }]}>
|
||||||
|
{(routingFields, { add: addUseWhen, remove: removeUseWhen }, { errors }) => (
|
||||||
|
<Space direction="vertical" size={8} className="agent-editor-tool-list">
|
||||||
|
<div className="agent-editor-tool-list-header">
|
||||||
|
<div>
|
||||||
|
<strong>useWhen</strong>
|
||||||
|
<div className="agent-editor-tool-help">至少一项,描述什么情况下应该调用这个 API。</div>
|
||||||
|
</div>
|
||||||
|
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addUseWhen('')}>
|
||||||
|
添加条件
|
||||||
|
</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={() => removeUseWhen(routingField.name)}
|
||||||
|
disabled={routingFields.length <= 1}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Form.ErrorList errors={errors} />
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
<Form.List name={[field.name, 'routing', 'doNotUseWhen']} rules={[{ validator: validateRoutingList('列表项不能为空') }]}>
|
||||||
|
{(routingFields, { add: addDoNotUseWhen, remove: removeDoNotUseWhen }, { errors }) => (
|
||||||
|
<Space direction="vertical" size={8} className="agent-editor-tool-list" style={{ marginTop: 16 }}>
|
||||||
|
<div className="agent-editor-tool-list-header">
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="agent-editor-tool-grid" style={{ marginTop: 16 }}>
|
||||||
|
<Form.Item label="domains(领域)" name={[field.name, 'routing', 'domains']}>
|
||||||
|
<Select mode="tags" placeholder="例如:e-commerce, logistics" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="intents(意图)" name={[field.name, 'routing', 'intents']}>
|
||||||
|
<Select mode="tags" placeholder="例如:query_order, cancel_order" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="agent-editor-tool-grid">
|
||||||
|
<Form.Item label="requiredSlots(必填槽位)" name={[field.name, 'routing', 'requiredSlots']}>
|
||||||
|
<Select mode="tags" placeholder="例如:order_id, user_id" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="optionalSlots(可选槽位)" name={[field.name, 'routing', 'optionalSlots']}>
|
||||||
|
<Select mode="tags" placeholder="例如:start_date, end_date" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.List name={[field.name, 'routing', 'examples']}>
|
||||||
|
{(exampleFields, { add: addExample, remove: removeExample }) => (
|
||||||
|
<Space direction="vertical" size={8} className="agent-editor-tool-list" style={{ marginTop: 16 }}>
|
||||||
|
<div className="agent-editor-tool-list-header">
|
||||||
|
<div>
|
||||||
|
<strong>examples(示例)</strong>
|
||||||
|
<div className="agent-editor-tool-help">描述用户可能的提问方式。</div>
|
||||||
|
</div>
|
||||||
|
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addExample('')}>
|
||||||
|
添加示例
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{exampleFields.map((exampleField) => (
|
||||||
|
<Space key={exampleField.key} align="start" className="agent-editor-tool-list">
|
||||||
|
<Form.Item
|
||||||
|
name={exampleField.name}
|
||||||
|
className="flex-1 mb-0"
|
||||||
|
rules={[{ validator: validateTrimmedText('示例不能为空') }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="我想查一下最近的订单" style={{ width: 600 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button danger type="text" icon={<MinusCircleOutlined />} onClick={() => removeExample(exampleField.name)} />
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
import { App as AntApp, Form, Modal, Tabs } from 'antd';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { AgentAPI, ExternalToolPlugin } from '../../api';
|
||||||
|
import { EMPTY_API, ExternalToolEditorProps, ToolApiFormValue, ToolPluginFormValue } from './types';
|
||||||
|
import { formValueToJson, formValueToPayload, jsonToFormValue } from './utils';
|
||||||
|
import VisualEditor from './VisualEditor';
|
||||||
|
import JsonEditor from './JsonEditor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部工具编辑弹窗。
|
||||||
|
* 支持可视化表单编辑与 JSON 直接编辑两种模式,双向实时同步。
|
||||||
|
*/
|
||||||
|
export default function ExternalToolEditor({ open, agentId, plugin, onClose, onSaved }: ExternalToolEditorProps) {
|
||||||
|
const { message } = AntApp.useApp();
|
||||||
|
const [form] = Form.useForm<ToolPluginFormValue>();
|
||||||
|
const isEditing = Boolean(plugin);
|
||||||
|
const apis = Form.useWatch('apis', form) || [];
|
||||||
|
const allValues = Form.useWatch([], form) as ToolPluginFormValue | undefined;
|
||||||
|
|
||||||
|
// 展开的 API 卡片索引
|
||||||
|
const [expandedApiIndexes, setExpandedApiIndexes] = useState<number[]>([]);
|
||||||
|
const previousApiCountRef = useRef(0);
|
||||||
|
|
||||||
|
// JSON 编辑相关状态
|
||||||
|
const [jsonContent, setJsonContent] = useState('');
|
||||||
|
const [activeTab, setActiveTab] = useState('visual');
|
||||||
|
// 是否正在由 JSON 侧同步表单,用于避免表单变更又反向写回 JSON 造成的循环
|
||||||
|
const isSyncingRef = useRef(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可视化表单变化 -> 同步到 JSON 文本
|
||||||
|
* 只有当不是由 JSON 侧触发的同步时才更新 JSON 内容,避免循环更新导致光标跳动
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!allValues || isSyncingRef.current) return;
|
||||||
|
try {
|
||||||
|
const jsonObj = formValueToJson(allValues);
|
||||||
|
setJsonContent(JSON.stringify(jsonObj, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
// 表单值不合法时不更新 JSON
|
||||||
|
}
|
||||||
|
}, [allValues]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 API 数量自动控制卡片展开状态。
|
||||||
|
*/
|
||||||
|
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]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 JSON 编辑区内容变化:解析后回填表单。
|
||||||
|
*/
|
||||||
|
const handleJsonChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setJsonContent(val);
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(val);
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
isSyncingRef.current = true;
|
||||||
|
form.setFieldsValue(jsonToFormValue(parsed));
|
||||||
|
// 下一个事件循环解除标记,恢复表单 -> JSON 的同步
|
||||||
|
setTimeout(() => {
|
||||||
|
isSyncingRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// JSON 格式不正确时不更新表单
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换 API 卡片展开/收起。
|
||||||
|
*/
|
||||||
|
const toggleApiCard = (index: number) => {
|
||||||
|
setExpandedApiIndexes((current) =>
|
||||||
|
current.includes(index) ? current.filter((item) => item !== index) : [...current, index],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交表单。
|
||||||
|
*/
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
const payload = formValueToPayload(values);
|
||||||
|
|
||||||
|
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 || '外部工具绑定失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗打开时初始化表单值。
|
||||||
|
*/
|
||||||
|
const handleAfterOpenChange = (visible: boolean) => {
|
||||||
|
if (!visible) return;
|
||||||
|
const initial: ToolPluginFormValue = plugin
|
||||||
|
? {
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
baseUrl: plugin.baseUrl,
|
||||||
|
authType: plugin.authType,
|
||||||
|
authConfig: JSON.stringify(plugin.authConfig || {}, null, 2),
|
||||||
|
headers: JSON.stringify(plugin.headers || {}, null, 2),
|
||||||
|
apis: plugin.apis.map((item): ToolApiFormValue => ({
|
||||||
|
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),
|
||||||
|
routing: {
|
||||||
|
summary: item.routing?.summary || '',
|
||||||
|
useWhen: item.routing?.useWhen?.length ? item.routing.useWhen : [''],
|
||||||
|
doNotUseWhen: item.routing?.doNotUseWhen || [],
|
||||||
|
domains: item.routing?.domains || [],
|
||||||
|
intents: item.routing?.intents || [],
|
||||||
|
requiredSlots: item.routing?.requiredSlots || [],
|
||||||
|
optionalSlots: item.routing?.optionalSlots || [],
|
||||||
|
examples: item.routing?.examples || [],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
baseUrl: '',
|
||||||
|
authType: 'bearer',
|
||||||
|
authConfig: JSON.stringify({ token: '' }, null, 2),
|
||||||
|
headers: JSON.stringify({}, null, 2),
|
||||||
|
apis: [{ ...EMPTY_API }],
|
||||||
|
};
|
||||||
|
|
||||||
|
isSyncingRef.current = true;
|
||||||
|
form.setFieldsValue(initial);
|
||||||
|
setTimeout(() => {
|
||||||
|
isSyncingRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title={isEditing ? '编辑外部工具集' : '配置外部工具集'}
|
||||||
|
width={920}
|
||||||
|
okText={isEditing ? '保存修改' : '绑定工具'}
|
||||||
|
cancelText="取消"
|
||||||
|
onCancel={onClose}
|
||||||
|
onOk={handleSubmit}
|
||||||
|
destroyOnHidden
|
||||||
|
afterOpenChange={handleAfterOpenChange}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" requiredMark="optional" preserve={true}>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'visual',
|
||||||
|
label: '可视化编辑',
|
||||||
|
children: (
|
||||||
|
<VisualEditor
|
||||||
|
form={form}
|
||||||
|
apis={apis}
|
||||||
|
expandedApiIndexes={expandedApiIndexes}
|
||||||
|
onToggleApiCard={toggleApiCard}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'json',
|
||||||
|
label: 'JSON 编辑',
|
||||||
|
children: <JsonEditor value={jsonContent} onChange={handleJsonChange} />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保持原类型 ExternalToolPlugin 的引用,避免 tree-shaking 丢失(其他地方可能依赖该类型重导出)
|
||||||
|
export type { ExternalToolPlugin };
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { ExternalToolApi, ExternalToolApiRouting, ExternalToolPlugin, ExternalToolPluginPayload } from '../../api';
|
||||||
|
|
||||||
|
/** 组件 Props 定义 */
|
||||||
|
export interface ExternalToolEditorProps {
|
||||||
|
/** 弹窗是否可见 */
|
||||||
|
open: boolean;
|
||||||
|
/** 智能体 ID */
|
||||||
|
agentId: string;
|
||||||
|
/** 已有的外部工具插件,传入时为编辑模式 */
|
||||||
|
plugin?: ExternalToolPlugin | null;
|
||||||
|
/** 关闭弹窗回调 */
|
||||||
|
onClose: () => void;
|
||||||
|
/** 保存成功后的回调 */
|
||||||
|
onSaved?: () => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个 API 路由规则的表单值(与 API 定义一致,仅做引用) */
|
||||||
|
export interface ToolApiRoutingFormValue extends ExternalToolApiRouting {}
|
||||||
|
|
||||||
|
/** 单个 API 的表单值
|
||||||
|
* - headers / parametersSchema 在表单中以字符串形式存储(Input.TextArea)
|
||||||
|
* - 提交时再通过 parseJsonObject 反序列化为对象
|
||||||
|
*/
|
||||||
|
export interface ToolApiFormValue extends Omit<ExternalToolApi, 'headers' | 'parametersSchema' | 'routing'> {
|
||||||
|
/** 请求头 JSON 字符串 */
|
||||||
|
headers?: string;
|
||||||
|
/** 依赖参数 Schema JSON 字符串 */
|
||||||
|
parametersSchema: string;
|
||||||
|
/** 路由规则 */
|
||||||
|
routing: ToolApiRoutingFormValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整个工具集的表单值
|
||||||
|
* - authConfig / headers 在表单中以字符串形式存储
|
||||||
|
*/
|
||||||
|
export interface ToolPluginFormValue extends Omit<ExternalToolPluginPayload, 'authConfig' | 'apis' | 'headers'> {
|
||||||
|
/** 认证配置 JSON 字符串 */
|
||||||
|
authConfig?: string;
|
||||||
|
/** 统一请求头 JSON 字符串 */
|
||||||
|
headers?: string;
|
||||||
|
/** API 列表 */
|
||||||
|
apis: ToolApiFormValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 空 API 模板 */
|
||||||
|
export const EMPTY_API: ToolApiFormValue = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
method: 'GET',
|
||||||
|
path: '',
|
||||||
|
headers: '{}',
|
||||||
|
parametersSchema: JSON.stringify({ type: 'object', properties: {} }, null, 2),
|
||||||
|
routing: {
|
||||||
|
summary: '',
|
||||||
|
useWhen: [''],
|
||||||
|
doNotUseWhen: [],
|
||||||
|
domains: [],
|
||||||
|
intents: [],
|
||||||
|
requiredSlots: [],
|
||||||
|
optionalSlots: [],
|
||||||
|
examples: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
import { ExternalToolApiRouting, ExternalToolPluginPayload } from '../../api';
|
||||||
|
import { ToolApiFormValue, ToolApiRoutingFormValue, ToolPluginFormValue } from './types';
|
||||||
|
|
||||||
|
/** 合法的认证类型枚举 */
|
||||||
|
const VALID_AUTH_TYPES = new Set(['none', 'bearer', 'basic', 'apiKey', 'custom']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 JSON 或类 JS 对象的字符串。
|
||||||
|
* - 优先尝试标准 JSON 解析
|
||||||
|
* - 失败后尝试作为 JS 对象字面量解析(支持无引号键、单引号等)
|
||||||
|
* - 再失败后尝试宽松解析(处理完全无引号的键值对)
|
||||||
|
* @param value 待解析的字符串
|
||||||
|
* @param fieldName 字段名称,用于错误提示
|
||||||
|
* @param optional 是否可选,为空时返回 undefined
|
||||||
|
* @returns 解析后的对象
|
||||||
|
* @throws Error 格式不正确时抛出异常
|
||||||
|
*/
|
||||||
|
export function parseJsonObject(value: string | undefined, fieldName: string, optional = false) {
|
||||||
|
const trimmedValue = value?.trim();
|
||||||
|
if (!trimmedValue) {
|
||||||
|
if (optional) return undefined;
|
||||||
|
throw new Error(`${fieldName}不能为空`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 优先尝试标准 JSON 解析
|
||||||
|
return JSON.parse(trimmedValue);
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
// 2. 失败后尝试作为 JS 对象解析 (支持无引号键、单引号等)
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
const parsed = new Function(`return (${trimmedValue})`)();
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
throw new Error();
|
||||||
|
} catch (e2) {
|
||||||
|
// 3. 尝试宽松解析 (处理类似 { Content-Type: application/json } 这种完全无引号的情况)
|
||||||
|
try {
|
||||||
|
const content = trimmedValue.replace(/^\{/, '').replace(/\}$/, '').trim();
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
const pairs = content.split(/[\n,]/);
|
||||||
|
let hasValidPair = false;
|
||||||
|
|
||||||
|
for (let pair of pairs) {
|
||||||
|
pair = pair.trim();
|
||||||
|
if (!pair) continue;
|
||||||
|
const colonIndex = pair.indexOf(':');
|
||||||
|
if (colonIndex > 0) {
|
||||||
|
const k = pair.substring(0, colonIndex).trim().replace(/^['"]|['"]$/g, '');
|
||||||
|
const v = pair.substring(colonIndex + 1).trim().replace(/^['"]|['"]$/g, '');
|
||||||
|
if (k) {
|
||||||
|
let finalVal: any = v;
|
||||||
|
if (v === 'true') finalVal = true;
|
||||||
|
else if (v === 'false') finalVal = false;
|
||||||
|
else if (v === 'null') finalVal = null;
|
||||||
|
else if (!isNaN(Number(v)) && v !== '') finalVal = Number(v);
|
||||||
|
result[k] = finalVal;
|
||||||
|
hasValidPair = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasValidPair) return result;
|
||||||
|
} catch (e3) {
|
||||||
|
// ignore and fall through to error
|
||||||
|
}
|
||||||
|
throw new Error(`${fieldName}格式不正确,请确保是有效的 JSON 或对象格式`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化字符串列表:去重、去空、trim。
|
||||||
|
* @param values 原始字符串数组
|
||||||
|
* @returns 规范化后的字符串数组
|
||||||
|
*/
|
||||||
|
export function normalizeStringList(values?: string[]) {
|
||||||
|
return Array.from(new Set((values ?? []).map((item) => item?.trim()).filter((item): item is string => Boolean(item))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文本字段 trim 校验器。
|
||||||
|
* @param message 错误提示
|
||||||
|
* @returns 校验函数
|
||||||
|
*/
|
||||||
|
export function validateTrimmedText(message: string) {
|
||||||
|
return async (_: unknown, value: string | undefined) => {
|
||||||
|
if (!value?.trim()) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成路由列表(字符串数组)校验器。
|
||||||
|
* @param message 错误提示
|
||||||
|
* @param min 最少项数
|
||||||
|
* @returns 校验函数
|
||||||
|
*/
|
||||||
|
export function validateRoutingList(message: string, min = 0) {
|
||||||
|
return async (_: unknown, value: string[] | undefined) => {
|
||||||
|
const normalized = normalizeStringList(value);
|
||||||
|
if (normalized.length < min) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((value ?? []).some((item) => !item?.trim())) {
|
||||||
|
throw new Error('列表项不能为空');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化单条 API 的路由规则。
|
||||||
|
* @param value 表单中的路由规则
|
||||||
|
* @param apiName API 名称(用于错误提示)
|
||||||
|
* @returns 规范化后的路由规则
|
||||||
|
*/
|
||||||
|
export function normalizeRouting(value: ToolApiRoutingFormValue | undefined, apiName: string): ExternalToolApiRouting {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`API ${apiName} 的路由规则不能为空`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWhen = normalizeStringList(value.useWhen);
|
||||||
|
if (useWhen.length < 1) {
|
||||||
|
throw new Error(`API ${apiName} 的 useWhen 至少保留一项`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: value.summary?.trim() || '',
|
||||||
|
useWhen,
|
||||||
|
doNotUseWhen: normalizeStringList(value.doNotUseWhen),
|
||||||
|
domains: normalizeStringList(value.domains),
|
||||||
|
intents: normalizeStringList(value.intents),
|
||||||
|
requiredSlots: normalizeStringList(value.requiredSlots),
|
||||||
|
optionalSlots: normalizeStringList(value.optionalSlots),
|
||||||
|
examples: normalizeStringList(value.examples),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将表单值转换为提交用的 Payload(字符串字段反序列化为对象)。
|
||||||
|
* @param values 表单值
|
||||||
|
* @returns 接口需要的 Payload
|
||||||
|
*/
|
||||||
|
export function formValueToPayload(values: ToolPluginFormValue): ExternalToolPluginPayload {
|
||||||
|
return {
|
||||||
|
name: values.name.trim(),
|
||||||
|
description: values.description?.trim(),
|
||||||
|
baseUrl: values.baseUrl.trim(),
|
||||||
|
authType: values.authType,
|
||||||
|
authConfig: parseJsonObject(values.authConfig, '认证配置', values.authType === 'none') || {},
|
||||||
|
headers: parseJsonObject(values.headers, '统一请求头', true),
|
||||||
|
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} 的依赖参数`),
|
||||||
|
routing: normalizeRouting(item.routing, item.name.trim() || '未命名 API'),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 JSON 对象(可能包含对象字段)转换为表单值(对象字段序列化为字符串)。
|
||||||
|
* 当用户在 JSON 编辑器中粘贴完整 JSON 后,调用此函数将其转换为可设置到 Form 的结构。
|
||||||
|
* @param json 原始 JSON 对象
|
||||||
|
* @returns 表单可用的值
|
||||||
|
*/
|
||||||
|
export function jsonToFormValue(json: Record<string, any>): ToolPluginFormValue {
|
||||||
|
const apisRaw: any[] = Array.isArray(json.apis) ? json.apis : [];
|
||||||
|
const apis: ToolApiFormValue[] = apisRaw.map((item) => ({
|
||||||
|
name: typeof item.name === 'string' ? item.name : '',
|
||||||
|
description: typeof item.description === 'string' ? item.description : '',
|
||||||
|
method: typeof item.method === 'string' ? item.method : 'GET',
|
||||||
|
path: typeof item.path === 'string' ? item.path : '',
|
||||||
|
headers: typeof item.headers === 'string' ? item.headers : JSON.stringify(item.headers || {}, null, 2),
|
||||||
|
parametersSchema:
|
||||||
|
typeof item.parametersSchema === 'string'
|
||||||
|
? item.parametersSchema
|
||||||
|
: JSON.stringify(item.parametersSchema || { type: 'object', properties: {} }, null, 2),
|
||||||
|
routing: {
|
||||||
|
summary: typeof item.routing?.summary === 'string' ? item.routing.summary : '',
|
||||||
|
useWhen: Array.isArray(item.routing?.useWhen) ? item.routing.useWhen : [''],
|
||||||
|
doNotUseWhen: Array.isArray(item.routing?.doNotUseWhen) ? item.routing.doNotUseWhen : [],
|
||||||
|
domains: Array.isArray(item.routing?.domains) ? item.routing.domains : [],
|
||||||
|
intents: Array.isArray(item.routing?.intents) ? item.routing.intents : [],
|
||||||
|
requiredSlots: Array.isArray(item.routing?.requiredSlots) ? item.routing.requiredSlots : [],
|
||||||
|
optionalSlots: Array.isArray(item.routing?.optionalSlots) ? item.routing.optionalSlots : [],
|
||||||
|
examples: Array.isArray(item.routing?.examples) ? item.routing.examples : [],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: typeof json.name === 'string' ? json.name : '',
|
||||||
|
description: typeof json.description === 'string' ? json.description : '',
|
||||||
|
baseUrl: typeof json.baseUrl === 'string' ? json.baseUrl : '',
|
||||||
|
authType: VALID_AUTH_TYPES.has(json.authType) ? json.authType : 'bearer',
|
||||||
|
authConfig: typeof json.authConfig === 'string' ? json.authConfig : JSON.stringify(json.authConfig || {}, null, 2),
|
||||||
|
headers: typeof json.headers === 'string' ? json.headers : JSON.stringify(json.headers || {}, null, 2),
|
||||||
|
apis: apis.length ? apis : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将表单值(字符串字段)转换为 JSON 编辑器显示的对象(字符串字段反序列化为对象)。
|
||||||
|
* 当可视化表单发生变化时,调用此函数生成 JSON 字符串展示给用户。
|
||||||
|
* @param values 表单值
|
||||||
|
* @returns 可序列化为 JSON 的对象
|
||||||
|
*/
|
||||||
|
export function formValueToJson(values: ToolPluginFormValue): Record<string, any> {
|
||||||
|
return {
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
baseUrl: values.baseUrl,
|
||||||
|
authType: values.authType,
|
||||||
|
authConfig: parseJsonObject(values.authConfig, '认证配置', true) || {},
|
||||||
|
headers: parseJsonObject(values.headers, '统一请求头', true) || {},
|
||||||
|
apis: (values.apis || []).map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
method: item.method,
|
||||||
|
path: item.path,
|
||||||
|
headers: parseJsonObject(item.headers, `API ${item.name || ''} 的请求头`, true) || {},
|
||||||
|
parametersSchema: parseJsonObject(item.parametersSchema, `API ${item.name || ''} 的依赖参数`, true) || {},
|
||||||
|
routing: item.routing || {},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue