This commit is contained in:
Xin Wang
2026-03-02 01:56:47 +08:00

View File

@@ -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<string, any>): Record<string, any> => {
const defaults: Record<string, any> = {};
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<string, any>).default;
}
}
return defaults;
};
export const ToolLibraryPage: React.FC = () => {
const [tools, setTools] = useState<Tool[]>([]);
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<ToolParameterDraft[]>([]);
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<string, any>;
try {
const parsed = JSON.parse(schemaEditorText || '{}');
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
setSchemaEditorError('Schema 必须是 JSON 对象。');
return;
}
parsedSchema = parsed as Record<string, any>;
} 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<string, any> = {
...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 = () => {
<Dialog
isOpen={isToolModalOpen}
onClose={() => setIsToolModalOpen(false)}
onClose={() => {
setIsToolModalOpen(false);
setSchemaDrawerOpen(false);
setSchemaEditorError('');
}}
title={editingTool ? '编辑自定义工具' : '添加自定义工具'}
contentClassName="max-w-4xl"
footer={
<>
<Button variant="ghost" onClick={() => setIsToolModalOpen(false)}></Button>
<Button
variant="ghost"
onClick={() => {
setIsToolModalOpen(false);
setSchemaDrawerOpen(false);
setSchemaEditorError('');
}}
>
</Button>
<Button onClick={handleSaveTool} disabled={saving}>{saving ? '保存中...' : (editingTool ? '保存修改' : '确认添加')}</Button>
</>
}
@@ -606,10 +721,15 @@ export const ToolLibraryPage: React.FC = () => {
<div className="space-y-4 rounded-md border border-white/10 bg-white/5 p-3">
<div className="flex items-center justify-between">
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-300">Tool Parameters</div>
<div className="flex items-center gap-2">
<Button type="button" variant="outline" size="sm" onClick={openSchemaDrawer}>
Schema
</Button>
<Button type="button" variant="outline" size="sm" onClick={addToolParameter}>
<Plus className="w-3.5 h-3.5 mr-1" />
</Button>
</div>
</div>
{toolParameters.length === 0 ? (
<div className="rounded-md border border-white/10 bg-black/20 p-3 text-xs text-muted-foreground">
JSON Schema
@@ -744,6 +864,39 @@ export const ToolLibraryPage: React.FC = () => {
</label>
</div>
</Dialog>
<Drawer
isOpen={schemaDrawerOpen}
onClose={() => {
setSchemaDrawerOpen(false);
setSchemaEditorError('');
}}
title="Tool Schema Editor"
>
<div className="space-y-3">
<p className="text-xs text-muted-foreground">
/ JSON Schema
</p>
<textarea
className="flex min-h-[58vh] w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white font-mono"
value={schemaEditorText}
onChange={(e) => {
setSchemaEditorText(e.target.value);
if (schemaEditorError) setSchemaEditorError('');
}}
placeholder='{"type":"object","properties":{},"required":[]}'
/>
{schemaEditorError && (
<div className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
{schemaEditorError}
</div>
)}
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" onClick={() => setSchemaDrawerOpen(false)}></Button>
<Button variant="outline" onClick={formatSchemaEditor}></Button>
<Button onClick={applySchemaEditor}></Button>
</div>
</div>
</Drawer>
</div>
);
};