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, Drawer } from '../components/UI'; import { Tool } from '../types'; import { createTool, deleteTool, fetchTools, updateTool } from '../services/backendApi'; const iconMap: Record = { Camera: , CameraOff: , Image: , Images: , CloudSun: , Calendar: , TrendingUp: , Coins: , Terminal: , Globe: , Wrench: , Box: , Volume2: , }; type ToolParameterType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array'; type ToolParameterDraft = { id: string; key: string; sourceKey: 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: '', sourceKey: '', 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, sourceKey: 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, sourceKey: 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 normalizeObjectSchema = (value: any): Record => { const schema = value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {}; const properties = schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties) ? schema.properties : {}; const required = Array.isArray(schema.required) ? schema.required.map((item: any) => String(item)) : []; return { ...schema, type: 'object', properties, required, }; }; const normalizeDefaultsObject = (value: any): Record => value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {}; const buildToolParameterConfig = ( drafts: ToolParameterDraft[], baseSchemaInput?: Record ): { schema: Record; defaults: Record; error?: string } => { const baseSchema = normalizeObjectSchema(baseSchemaInput); const baseProperties = baseSchema.properties && typeof baseSchema.properties === 'object' && !Array.isArray(baseSchema.properties) ? (baseSchema.properties as Record) : {}; 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 (seen.has(key)) { return { schema: {}, defaults: {}, error: `参数标识重复: ${key}` }; } seen.add(key); const sourceKey = String(draft.sourceKey || '').trim(); const preservedRaw = ( (sourceKey && Object.prototype.hasOwnProperty.call(baseProperties, sourceKey) ? baseProperties[sourceKey] : undefined) ?? baseProperties[key] ); const preservedSpec = preservedRaw && typeof preservedRaw === 'object' && !Array.isArray(preservedRaw) ? { ...(preservedRaw as Record) } : {}; properties[key] = preservedSpec; properties[key].type = draft.type; if (draft.description.trim()) { properties[key].description = draft.description.trim(); } else { delete properties[key].description; } 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; properties[key].default = parsed.value; } else { delete properties[key].default; } } return { schema: { ...baseSchema, type: 'object', properties, required, }, defaults, }; }; 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(''); const [categoryFilter, setCategoryFilter] = useState<'all' | 'system' | 'query'>('all'); const [isLoading, setIsLoading] = useState(true); const [isToolModalOpen, setIsToolModalOpen] = useState(false); const [editingTool, setEditingTool] = useState(null); const [toolName, setToolName] = useState(''); const [toolId, setToolId] = useState(''); const [toolDesc, setToolDesc] = useState(''); const [toolCategory, setToolCategory] = useState<'system' | 'query'>('system'); const [toolIcon, setToolIcon] = useState('Wrench'); const [toolEnabled, setToolEnabled] = useState(true); const [toolWaitForResponse, setToolWaitForResponse] = useState(false); const [toolHttpMethod, setToolHttpMethod] = useState<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>('GET'); const [toolHttpUrl, setToolHttpUrl] = useState(''); const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}'); const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000); const [toolSchema, setToolSchema] = useState>({ type: 'object', properties: {}, required: [] }); const [toolParameterDefaults, setToolParameterDefaults] = useState>({}); 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 () => { setIsLoading(true); try { setTools(await fetchTools()); } catch (error) { console.error(error); setTools([]); } finally { setIsLoading(false); } }; useEffect(() => { loadTools(); }, []); const openAdd = () => { setEditingTool(null); setToolName(''); setToolId(''); setToolDesc(''); setToolCategory('system'); setToolIcon('Wrench'); setToolEnabled(true); setToolWaitForResponse(false); setToolHttpMethod('GET'); setToolHttpUrl(''); setToolHttpHeadersText('{}'); setToolHttpTimeoutMs(10000); const emptySchema = { type: 'object', properties: {}, required: [] }; setToolSchema(emptySchema); setToolParameterDefaults({}); setToolParameters([]); setSchemaEditorError(''); setSchemaEditorText(JSON.stringify(emptySchema, null, 2)); setSchemaDrawerOpen(false); setIsToolModalOpen(true); }; const openEdit = (tool: Tool) => { setEditingTool(tool); setToolName(tool.name); setToolId(tool.id); setToolDesc(tool.description || ''); setToolCategory(tool.category); setToolIcon(tool.icon || 'Wrench'); setToolEnabled(tool.enabled ?? true); setToolWaitForResponse(Boolean(tool.waitForResponse)); setToolHttpMethod((tool.httpMethod || 'GET') as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'); setToolHttpUrl(tool.httpUrl || ''); setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2)); setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000); const normalizedSchema = normalizeObjectSchema(tool.parameterSchema); const mergedDefaults = { ...defaultsFromSchema(normalizedSchema), ...normalizeDefaultsObject(tool.parameterDefaults), }; setToolSchema(normalizedSchema); setToolParameterDefaults(mergedDefaults); setToolParameters(draftsFromSchema(normalizedSchema, mergedDefaults)); setSchemaEditorError(''); setSchemaEditorText( JSON.stringify( normalizedSchema, null, 2 ) ); setSchemaDrawerOpen(false); setIsToolModalOpen(true); }; const openSchemaDrawer = () => { const parameterConfig = buildToolParameterConfig(toolParameters, toolSchema); if (!parameterConfig.error) { setToolSchema(parameterConfig.schema); setToolParameterDefaults(parameterConfig.defaults); setToolParameters((prev) => prev.map((item) => ({ ...item, sourceKey: item.key.trim() || item.sourceKey }))); setSchemaEditorText(JSON.stringify(parameterConfig.schema, null, 2)); } else if (toolSchema && typeof toolSchema === 'object') { setSchemaEditorText(JSON.stringify(toolSchema, 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 = normalizeObjectSchema(parsedSchema); const mergedDefaults = { ...normalizeDefaultsObject(toolParameterDefaults), ...defaultsFromSchema(normalizedSchema) }; setToolSchema(normalizedSchema); setToolParameterDefaults(mergedDefaults); setToolParameters(draftsFromSchema(normalizedSchema, mergedDefaults)); setSchemaEditorError(''); setSchemaEditorText(JSON.stringify(normalizedSchema, null, 2)); setSchemaDrawerOpen(false); }; const filteredTools = tools.filter((tool) => { const q = searchTerm.toLowerCase(); const matchesSearch = tool.name.toLowerCase().includes(q) || (tool.description || '').toLowerCase().includes(q) || tool.id.toLowerCase().includes(q); const matchesCategory = categoryFilter === 'all' || tool.category === categoryFilter; return matchesSearch && matchesCategory; }); const systemTools = filteredTools.filter((tool) => tool.category === 'system'); const queryTools = filteredTools.filter((tool) => tool.category === 'query'); const renderToolCard = (tool: Tool) => (
{iconMap[tool.icon] || }
{tool.name} {tool.isSystem ? SYSTEM : CUSTOM}
{tool.category === 'system' ? 'SYSTEM' : 'QUERY'} {tool.category === 'system' && ( {tool.waitForResponse ? 'WAIT' : 'NO-WAIT'} )} ID: {tool.id}

{tool.description}

system/query 仅表示执行类型
); 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('请填写工具名称'); return; } if (!editingTool && toolId.trim() && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(toolId.trim())) { alert('工具 ID 不合法,请使用字母/数字/下划线,且不能以数字开头'); return; } try { setSaving(true); let parsedHeaders: Record = {}; const parameterConfig = buildToolParameterConfig(toolParameters, toolSchema); if (parameterConfig.error) { alert(parameterConfig.error); setSaving(false); return; } const parsedParameterSchema = parameterConfig.schema; const parsedParameterDefaults = parameterConfig.defaults; setToolSchema(parsedParameterSchema); setToolParameterDefaults(parsedParameterDefaults); setToolParameters((prev) => prev.map((item) => ({ ...item, sourceKey: item.key.trim() || item.sourceKey }))); setSchemaEditorText(JSON.stringify(parsedParameterSchema, null, 2)); if (toolCategory === 'query') { if ( !toolHttpUrl.trim() && editingTool?.id !== 'calculator' && editingTool?.id !== 'code_interpreter' && editingTool?.id !== 'current_time' ) { alert('信息查询工具请填写 HTTP URL'); setSaving(false); return; } try { const parsed = JSON.parse(toolHttpHeadersText || '{}'); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { parsedHeaders = parsed as Record; } else { throw new Error('headers must be object'); } } catch { alert('HTTP Headers 必须是合法 JSON 对象'); setSaving(false); return; } } if (editingTool) { const updated = await updateTool(editingTool.id, { name: toolName.trim(), description: toolDesc, category: toolCategory, icon: toolIcon, httpMethod: toolHttpMethod, httpUrl: toolHttpUrl.trim(), httpHeaders: parsedHeaders, httpTimeoutMs: toolHttpTimeoutMs, parameterSchema: parsedParameterSchema, parameterDefaults: parsedParameterDefaults, waitForResponse: toolCategory === 'system' ? toolWaitForResponse : false, enabled: toolEnabled, }); setTools((prev) => prev.map((item) => (item.id === updated.id ? updated : item))); } else { const created = await createTool({ id: toolId.trim() || undefined, name: toolName.trim(), description: toolDesc, category: toolCategory, icon: toolIcon, httpMethod: toolHttpMethod, httpUrl: toolHttpUrl.trim(), httpHeaders: parsedHeaders, httpTimeoutMs: toolHttpTimeoutMs, parameterSchema: parsedParameterSchema, parameterDefaults: parsedParameterDefaults, waitForResponse: toolCategory === 'system' ? toolWaitForResponse : false, enabled: toolEnabled, }); setTools((prev) => [created, ...prev]); } setIsToolModalOpen(false); } catch (error: any) { alert(error?.message || '保存工具失败'); } finally { setSaving(false); } }; const handleDeleteTool = async (e: React.MouseEvent, tool: Tool) => { e.stopPropagation(); if (!confirm('确认删除该工具吗?')) return; try { await deleteTool(tool.id); setTools((prev) => prev.filter((item) => item.id !== tool.id)); } catch (error: any) { alert(error?.message || '删除失败'); } }; return (

工具与插件

setSearchTerm(e.target.value)} />
{isLoading ? (

加载中...

) : filteredTools.length === 0 ? (

未找到相关工具

) : (
{(categoryFilter === 'all' || categoryFilter === 'system') && (

System Command

{systemTools.length} tools
{systemTools.length === 0 ? (
当前筛选条件下无系统指令工具。
) : (
{systemTools.map(renderToolCard)}
)}
)} {(categoryFilter === 'all' || categoryFilter === 'query') && (

Information Query

{queryTools.length} tools
{queryTools.length === 0 ? (
当前筛选条件下无信息查询工具。
) : (
{queryTools.map(renderToolCard)}
)}
)}
)} { setIsToolModalOpen(false); setSchemaDrawerOpen(false); setSchemaEditorError(''); }} title={editingTool ? '编辑自定义工具' : '添加自定义工具'} contentClassName="max-w-4xl" footer={ <> } >
setToolName(e.target.value)} placeholder="例如: 智能家居控制" autoFocus />
setToolId(e.target.value)} placeholder="例如: voice_msg_prompt(留空自动生成)" disabled={Boolean(editingTool)} />

{editingTool ? '已创建工具的 ID 不可修改。' : '建议客户端工具填写稳定 ID,避免随机 tool_xxx 导致前端无法识别。'}