diff --git a/web/pages/ToolLibrary.tsx b/web/pages/ToolLibrary.tsx index 84074d8..2738cfd 100644 --- a/web/pages/ToolLibrary.tsx +++ b/web/pages/ToolLibrary.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Search, Filter, Plus, Wrench, Terminal, Globe, Camera, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Trash2, Edit2, Box, Volume2 } from 'lucide-react'; -import { Button, Input, Badge, Dialog } from '../components/UI'; +import { Button, Input, Badge, Dialog, Drawer } from '../components/UI'; import { Tool } from '../types'; import { createTool, deleteTool, fetchTools, updateTool } from '../services/backendApi'; @@ -170,6 +170,19 @@ const buildToolParameterConfig = (drafts: ToolParameterDraft[]): { schema: Recor }; }; +const defaultsFromSchema = (schema: Record): Record => { + const defaults: Record = {}; + const properties = schema?.properties; + if (!properties || typeof properties !== 'object' || Array.isArray(properties)) return defaults; + for (const [key, spec] of Object.entries(properties)) { + if (!spec || typeof spec !== 'object' || Array.isArray(spec)) continue; + if (Object.prototype.hasOwnProperty.call(spec, 'default')) { + defaults[key] = (spec as Record).default; + } + } + return defaults; +}; + export const ToolLibraryPage: React.FC = () => { const [tools, setTools] = useState([]); const [searchTerm, setSearchTerm] = useState(''); @@ -190,6 +203,9 @@ export const ToolLibraryPage: React.FC = () => { const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}'); const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000); const [toolParameters, setToolParameters] = useState([]); + const [schemaDrawerOpen, setSchemaDrawerOpen] = useState(false); + const [schemaEditorText, setSchemaEditorText] = useState(''); + const [schemaEditorError, setSchemaEditorError] = useState(''); const [saving, setSaving] = useState(false); const loadTools = async () => { @@ -222,6 +238,9 @@ export const ToolLibraryPage: React.FC = () => { setToolHttpHeadersText('{}'); setToolHttpTimeoutMs(10000); setToolParameters([]); + setSchemaEditorError(''); + setSchemaEditorText(JSON.stringify({ type: 'object', properties: {}, required: [] }, null, 2)); + setSchemaDrawerOpen(false); setIsToolModalOpen(true); }; @@ -239,9 +258,92 @@ export const ToolLibraryPage: React.FC = () => { setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2)); setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000); setToolParameters(draftsFromSchema(tool.parameterSchema, tool.parameterDefaults)); + setSchemaEditorError(''); + setSchemaEditorText( + JSON.stringify( + (tool.parameterSchema && typeof tool.parameterSchema === 'object') + ? tool.parameterSchema + : { type: 'object', properties: {}, required: [] }, + null, + 2 + ) + ); + setSchemaDrawerOpen(false); setIsToolModalOpen(true); }; + const openSchemaDrawer = () => { + const parameterConfig = buildToolParameterConfig(toolParameters); + if (!parameterConfig.error) { + setSchemaEditorText(JSON.stringify(parameterConfig.schema, null, 2)); + } else if (editingTool?.parameterSchema && typeof editingTool.parameterSchema === 'object') { + setSchemaEditorText(JSON.stringify(editingTool.parameterSchema, null, 2)); + } else if (!schemaEditorText.trim()) { + setSchemaEditorText(JSON.stringify({ type: 'object', properties: {}, required: [] }, null, 2)); + } + setSchemaEditorError(''); + setSchemaDrawerOpen(true); + }; + + const formatSchemaEditor = () => { + try { + const parsed = JSON.parse(schemaEditorText || '{}'); + setSchemaEditorText(JSON.stringify(parsed, null, 2)); + setSchemaEditorError(''); + } catch { + setSchemaEditorError('Schema 不是合法 JSON,无法格式化。'); + } + }; + + const applySchemaEditor = () => { + let parsedSchema: Record; + try { + const parsed = JSON.parse(schemaEditorText || '{}'); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + setSchemaEditorError('Schema 必须是 JSON 对象。'); + return; + } + parsedSchema = parsed as Record; + } catch { + setSchemaEditorError('Schema 不是合法 JSON。'); + return; + } + + if (parsedSchema.type !== undefined && parsedSchema.type !== 'object') { + setSchemaEditorError("Schema.type 必须为 'object'。"); + return; + } + if ( + parsedSchema.properties !== undefined + && (!parsedSchema.properties || typeof parsedSchema.properties !== 'object' || Array.isArray(parsedSchema.properties)) + ) { + setSchemaEditorError('Schema.properties 必须是对象。'); + return; + } + if (parsedSchema.required !== undefined && !Array.isArray(parsedSchema.required)) { + setSchemaEditorError('Schema.required 必须是数组。'); + return; + } + + const normalizedSchema: Record = { + ...parsedSchema, + type: 'object', + properties: parsedSchema.properties && typeof parsedSchema.properties === 'object' && !Array.isArray(parsedSchema.properties) + ? parsedSchema.properties + : {}, + required: Array.isArray(parsedSchema.required) ? parsedSchema.required : [], + }; + + const currentParameterConfig = buildToolParameterConfig(toolParameters); + const currentDefaults = currentParameterConfig.error ? {} : currentParameterConfig.defaults; + const mergedDefaults = { ...currentDefaults, ...defaultsFromSchema(normalizedSchema) }; + + setToolParameters(draftsFromSchema(normalizedSchema, mergedDefaults)); + setSchemaEditorError(''); + setSchemaEditorText(JSON.stringify(normalizedSchema, null, 2)); + setSchemaDrawerOpen(false); + }; + const filteredTools = tools.filter((tool) => { const q = searchTerm.toLowerCase(); const matchesSearch = @@ -506,12 +608,25 @@ export const ToolLibraryPage: React.FC = () => { setIsToolModalOpen(false)} + onClose={() => { + setIsToolModalOpen(false); + setSchemaDrawerOpen(false); + setSchemaEditorError(''); + }} title={editingTool ? '编辑自定义工具' : '添加自定义工具'} contentClassName="max-w-4xl" footer={ <> - + } @@ -606,9 +721,14 @@ export const ToolLibraryPage: React.FC = () => {
Tool Parameters
- +
+ + +
{toolParameters.length === 0 ? (
@@ -744,6 +864,39 @@ export const ToolLibraryPage: React.FC = () => {
+ { + setSchemaDrawerOpen(false); + setSchemaEditorError(''); + }} + title="Tool Schema Editor" + > +
+

+ 可在此粘贴/编辑 JSON Schema。点击「应用到参数表单」后会同步到上方参数编辑区。 +

+