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
-
- -