import { CopyOutlined, DownOutlined, MinusCircleOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons'; 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'; interface Props { open: boolean; agentId: string; plugin?: ExternalToolPlugin | null; onClose: () => void; onSaved?: () => void | Promise; } interface ToolApiRoutingFormValue extends ExternalToolApiRouting {} interface ToolApiFormValue extends Omit { headers?: string; parametersSchema: string; routing: ToolApiRoutingFormValue; } interface ToolPluginFormValue extends Omit { 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 = {}; 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(); const isEditing = Boolean(plugin); const apis = Form.useWatch('apis', form) || []; const [expandedApiIndexes, setExpandedApiIndexes] = useState([]); 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 () => { 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 ( { 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 }], }, ); } }} >
{(routingFields, { add: addUseWhen, remove: removeUseWhen }, { errors }) => (
useWhen
至少一项,描述什么情况下应该调用这个 API。
{routingFields.map((routingField) => (
{routingFields.map((routingField) => ( {exampleFields.map((exampleField) => (