From 487634c4941697ed42ff45a2f31b86a2d52c0695 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 27 Feb 2026 15:04:52 +0800 Subject: [PATCH] Implement tool parameter management in ToolLibrary, including types, default value handling, and schema generation. Introduce functions for creating, updating, and removing tool parameters, enhancing the configuration capabilities for tools. Update state management to reflect new parameter structure. --- web/pages/ToolLibrary.tsx | 316 ++++++++++++++++++++++++++++++-------- 1 file changed, 251 insertions(+), 65 deletions(-) diff --git a/web/pages/ToolLibrary.tsx b/web/pages/ToolLibrary.tsx index 4a07f06..8c6b898 100644 --- a/web/pages/ToolLibrary.tsx +++ b/web/pages/ToolLibrary.tsx @@ -20,11 +20,155 @@ const iconMap: Record = { Volume2: , }; -const DEFAULT_PARAMETER_SCHEMA_TEXT = JSON.stringify( - { type: 'object', properties: {}, required: [] }, - null, - 2 -); +type ToolParameterType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array'; +type ToolParameterDraft = { + id: string; + key: string; + type: ToolParameterType; + required: boolean; + description: string; + hasDefault: boolean; + defaultValueText: string; +}; + +const TOOL_PARAMETER_TYPES: ToolParameterType[] = ['string', 'number', 'integer', 'boolean', 'object', 'array']; + +const newParameterDraft = (): ToolParameterDraft => ({ + id: `param_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`, + key: '', + type: 'string', + required: false, + description: '', + hasDefault: false, + defaultValueText: '', +}); + +const schemaTypeOrDefault = (value: any): ToolParameterType => + TOOL_PARAMETER_TYPES.includes(value as ToolParameterType) ? (value as ToolParameterType) : 'string'; + +const stringifyDefaultValue = (value: any): string => { + if (value === null || value === undefined) return ''; + if (typeof value === 'object') { + try { + return JSON.stringify(value); + } catch { + return ''; + } + } + return String(value); +}; + +const parseDefaultValueByType = (type: ToolParameterType, valueText: string): { value?: any; error?: string } => { + const text = valueText.trim(); + if (type === 'string') return { value: valueText }; + if (type === 'number') { + const n = Number(text); + if (!Number.isFinite(n)) return { error: `参数默认值不是合法 number: ${valueText}` }; + return { value: n }; + } + if (type === 'integer') { + const n = Number(text); + if (!Number.isInteger(n)) return { error: `参数默认值不是合法 integer: ${valueText}` }; + return { value: n }; + } + if (type === 'boolean') { + const lowered = text.toLowerCase(); + if (lowered === 'true') return { value: true }; + if (lowered === 'false') return { value: false }; + return { error: `参数默认值不是合法 boolean(true/false): ${valueText}` }; + } + if (type === 'object' || type === 'array') { + try { + const parsed = JSON.parse(text || (type === 'array' ? '[]' : '{}')); + if (type === 'object' && (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))) { + return { error: `参数默认值不是合法 object JSON: ${valueText}` }; + } + if (type === 'array' && !Array.isArray(parsed)) { + return { error: `参数默认值不是合法 array JSON: ${valueText}` }; + } + return { value: parsed }; + } catch { + return { error: `参数默认值不是合法 JSON: ${valueText}` }; + } + } + return { value: valueText }; +}; + +const draftsFromSchema = (schema: Record | undefined, defaults: Record | undefined): ToolParameterDraft[] => { + const safeSchema = schema && typeof schema === 'object' ? schema : {}; + const safeDefaults = defaults && typeof defaults === 'object' ? defaults : {}; + const properties = safeSchema.properties && typeof safeSchema.properties === 'object' ? safeSchema.properties : {}; + const requiredSet = new Set(Array.isArray(safeSchema.required) ? safeSchema.required.map((item) => String(item)) : []); + + const drafts: ToolParameterDraft[] = Object.entries(properties).map(([key, spec]) => { + const typedSpec: Record = spec && typeof spec === 'object' ? (spec as Record) : {}; + const hasDefault = Object.prototype.hasOwnProperty.call(safeDefaults, key); + return { + id: `param_${key}_${Math.random().toString(16).slice(2, 8)}`, + key, + type: schemaTypeOrDefault(typedSpec.type), + required: requiredSet.has(key), + description: String(typedSpec.description || ''), + hasDefault, + defaultValueText: hasDefault ? stringifyDefaultValue(safeDefaults[key]) : '', + }; + }); + + for (const [key, value] of Object.entries(safeDefaults)) { + if (drafts.some((item) => item.key === key)) continue; + drafts.push({ + id: `param_${key}_${Math.random().toString(16).slice(2, 8)}`, + key, + type: typeof value === 'number' && Number.isInteger(value) ? 'integer' : (typeof value as ToolParameterType) || 'string', + required: false, + description: '', + hasDefault: true, + defaultValueText: stringifyDefaultValue(value), + }); + } + return drafts; +}; + +const buildToolParameterConfig = (drafts: ToolParameterDraft[]): { schema: Record; defaults: Record; error?: string } => { + const properties: Record = {}; + const required: string[] = []; + const defaults: Record = {}; + const seen = new Set(); + + for (const draft of drafts) { + const key = draft.key.trim(); + if (!key) continue; + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) { + return { schema: {}, defaults: {}, error: `参数标识不合法: ${key}` }; + } + if (seen.has(key)) { + return { schema: {}, defaults: {}, error: `参数标识重复: ${key}` }; + } + seen.add(key); + + properties[key] = { + type: draft.type, + ...(draft.description.trim() ? { description: draft.description.trim() } : {}), + }; + if (draft.required) required.push(key); + if (draft.hasDefault) { + const parsed = parseDefaultValueByType(draft.type, draft.defaultValueText); + if (parsed.error) { + return { schema: {}, defaults: {}, error: parsed.error }; + } + defaults[key] = parsed.value; + } + } + + return { + schema: { + type: 'object', + properties, + required, + }, + defaults, + }; +}; export const ToolLibraryPage: React.FC = () => { const [tools, setTools] = useState([]); @@ -43,8 +187,7 @@ export const ToolLibraryPage: React.FC = () => { const [toolHttpUrl, setToolHttpUrl] = useState(''); const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}'); const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000); - const [toolParameterSchemaText, setToolParameterSchemaText] = useState(DEFAULT_PARAMETER_SCHEMA_TEXT); - const [toolParameterDefaultsText, setToolParameterDefaultsText] = useState('{}'); + const [toolParameters, setToolParameters] = useState([]); const [saving, setSaving] = useState(false); const loadTools = async () => { @@ -74,8 +217,7 @@ export const ToolLibraryPage: React.FC = () => { setToolHttpUrl(''); setToolHttpHeadersText('{}'); setToolHttpTimeoutMs(10000); - setToolParameterSchemaText(DEFAULT_PARAMETER_SCHEMA_TEXT); - setToolParameterDefaultsText('{}'); + setToolParameters([]); setIsToolModalOpen(true); }; @@ -90,8 +232,7 @@ export const ToolLibraryPage: React.FC = () => { setToolHttpUrl(tool.httpUrl || ''); setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2)); setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000); - setToolParameterSchemaText(JSON.stringify(tool.parameterSchema || { type: 'object', properties: {}, required: [] }, null, 2)); - setToolParameterDefaultsText(JSON.stringify(tool.parameterDefaults || {}, null, 2)); + setToolParameters(draftsFromSchema(tool.parameterSchema, tool.parameterDefaults)); setIsToolModalOpen(true); }; @@ -156,6 +297,18 @@ export const ToolLibraryPage: React.FC = () => { ); + const addToolParameter = () => { + setToolParameters((prev) => [...prev, newParameterDraft()]); + }; + + const updateToolParameter = (parameterId: string, patch: Partial) => { + setToolParameters((prev) => prev.map((item) => (item.id === parameterId ? { ...item, ...patch } : item))); + }; + + const removeToolParameter = (parameterId: string) => { + setToolParameters((prev) => prev.filter((item) => item.id !== parameterId)); + }; + const handleSaveTool = async () => { if (!toolName.trim()) { alert('请填写工具名称'); @@ -165,44 +318,14 @@ export const ToolLibraryPage: React.FC = () => { try { setSaving(true); let parsedHeaders: Record = {}; - let parsedParameterSchema: Record = {}; - let parsedParameterDefaults: Record = {}; - - try { - const parsed = JSON.parse(toolParameterSchemaText || '{}'); - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - throw new Error('schema must be object'); - } - parsedParameterSchema = parsed as Record; - } catch { - alert('参数 Schema 必须是合法 JSON 对象'); - setSaving(false); - return; - } - if (parsedParameterSchema.type && parsedParameterSchema.type !== 'object') { - alert("参数 Schema 的 type 必须是 'object'"); - setSaving(false); - return; - } - if (!parsedParameterSchema.type) parsedParameterSchema.type = 'object'; - if (!parsedParameterSchema.properties || typeof parsedParameterSchema.properties !== 'object' || Array.isArray(parsedParameterSchema.properties)) { - parsedParameterSchema.properties = {}; - } - if (!Array.isArray(parsedParameterSchema.required)) { - parsedParameterSchema.required = []; - } - - try { - const parsed = JSON.parse(toolParameterDefaultsText || '{}'); - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - throw new Error('defaults must be object'); - } - parsedParameterDefaults = parsed as Record; - } catch { - alert('参数默认值必须是合法 JSON 对象'); + const parameterConfig = buildToolParameterConfig(toolParameters); + if (parameterConfig.error) { + alert(parameterConfig.error); setSaving(false); return; } + const parsedParameterSchema = parameterConfig.schema; + const parsedParameterDefaults = parameterConfig.defaults; if (toolCategory === 'query') { if ( @@ -370,7 +493,7 @@ export const ToolLibraryPage: React.FC = () => { } > -
+
@@ -429,25 +552,88 @@ export const ToolLibraryPage: React.FC = () => {
-
Tool Parameters
-
- -