From 67e69d4d3b4a90c2f99458abb6393affa27ebe33 Mon Sep 17 00:00:00 2001 From: sp mac bookpro 2605 Date: Wed, 29 Jul 2026 13:42:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20JSON=20=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E5=90=8C=E6=AD=A5=E6=97=B6=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E6=8B=86=E5=88=86=E6=A8=A1=E5=9D=97=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=A1=8C=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trae/rules/rule.md | 1 + src/api/agents.ts | 2 +- src/components/ExternalToolEditor.tsx | 603 ------------------ .../ExternalToolEditor/JsonEditor.tsx | 31 + .../ExternalToolEditor/VisualEditor.tsx | 281 ++++++++ src/components/ExternalToolEditor/index.tsx | 223 +++++++ src/components/ExternalToolEditor/types.ts | 63 ++ src/components/ExternalToolEditor/utils.ts | 231 +++++++ 8 files changed, 831 insertions(+), 604 deletions(-) delete mode 100644 src/components/ExternalToolEditor.tsx create mode 100644 src/components/ExternalToolEditor/JsonEditor.tsx create mode 100644 src/components/ExternalToolEditor/VisualEditor.tsx create mode 100644 src/components/ExternalToolEditor/index.tsx create mode 100644 src/components/ExternalToolEditor/types.ts create mode 100644 src/components/ExternalToolEditor/utils.ts diff --git a/.trae/rules/rule.md b/.trae/rules/rule.md index 6342d13..2019b55 100644 --- a/.trae/rules/rule.md +++ b/.trae/rules/rule.md @@ -10,5 +10,6 @@ alwaysApply: true 4. **语言** - 永远使用中文跟用户沟通,专有名词可以用英文。 5. **文档** - 在用户没有明确要求先提供文档时,禁止创建文档。避免生产垃圾文件。用户明确要求提供文档时,才创建文档。 6. **注释** - 你生成的代码,尽可能完善中文注释。注释的格式需要按照Go语言的注释规范。要描述清楚代码的功能,参数,返回值,异常等。 +7. **代码格式** - .go文件,.ts, .tsx 文件,单文件代码不超过300行,当超过300行时,需要做拆分,按功能模块拆分,同模块在同一个文件夹下,文件夹名要语义化。 diff --git a/src/api/agents.ts b/src/api/agents.ts index b47165e..663c980 100644 --- a/src/api/agents.ts +++ b/src/api/agents.ts @@ -60,7 +60,7 @@ export interface ExternalToolPluginPayload { name: string; description?: string; baseUrl: string; - authType: 'none' | 'bearer' | 'basic' | 'apiKey'; + authType: 'none' | 'bearer' | 'basic' | 'apiKey' | 'custom'; authConfig: Record; headers?: Record | null; apis: ExternalToolApi[]; diff --git a/src/components/ExternalToolEditor.tsx b/src/components/ExternalToolEditor.tsx deleted file mode 100644 index fda230c..0000000 --- a/src/components/ExternalToolEditor.tsx +++ /dev/null @@ -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; -} - -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 allValues = Form.useWatch([], form); - const [expandedApiIndexes, setExpandedApiIndexes] = useState([]); - 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) => { - 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 ( - { - 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) => ( - - - - - + + {routingFields.map((routingField) => ( + + + + + + + {routingFields.map((routingField) => ( + + + + + + + {exampleFields.map((exampleField) => ( + + + + +