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 { 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; 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([]); 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 [toolParameters, setToolParameters] = 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); setToolParameters([]); 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); setToolParameters(draftsFromSchema(tool.parameterSchema, tool.parameterDefaults)); setIsToolModalOpen(true); }; 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); if (parameterConfig.error) { alert(parameterConfig.error); setSaving(false); return; } const parsedParameterSchema = parameterConfig.schema; const parsedParameterDefaults = parameterConfig.defaults; 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)} title={editingTool ? '编辑自定义工具' : '添加自定义工具'} contentClassName="max-w-4xl" footer={ <> } >
setToolName(e.target.value)} placeholder="例如: 智能家居控制" autoFocus />
setToolId(e.target.value)} placeholder="例如: voice_message_prompt(留空自动生成)" disabled={Boolean(editingTool)} />

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