Implement tool parameter management in ToolLibrary, including types, default value handling, and schema generation. Introduce functions for creating, updating, and removing tool parameters, enhancing the configuration capabilities for tools. Update state management to reflect new parameter structure.
This commit is contained in:
@@ -20,11 +20,155 @@ const iconMap: Record<string, React.ReactNode> = {
|
|||||||
Volume2: <Volume2 className="w-5 h-5" />,
|
Volume2: <Volume2 className="w-5 h-5" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PARAMETER_SCHEMA_TEXT = JSON.stringify(
|
type ToolParameterType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array';
|
||||||
{ type: 'object', properties: {}, required: [] },
|
type ToolParameterDraft = {
|
||||||
null,
|
id: string;
|
||||||
2
|
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<string, any> | undefined, defaults: Record<string, any> | 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<string>(Array.isArray(safeSchema.required) ? safeSchema.required.map((item) => String(item)) : []);
|
||||||
|
|
||||||
|
const drafts: ToolParameterDraft[] = Object.entries(properties).map(([key, spec]) => {
|
||||||
|
const typedSpec: Record<string, any> = spec && typeof spec === 'object' ? (spec as Record<string, any>) : {};
|
||||||
|
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<string, any>; defaults: Record<string, any>; error?: string } => {
|
||||||
|
const properties: Record<string, any> = {};
|
||||||
|
const required: string[] = [];
|
||||||
|
const defaults: Record<string, any> = {};
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
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 = () => {
|
export const ToolLibraryPage: React.FC = () => {
|
||||||
const [tools, setTools] = useState<Tool[]>([]);
|
const [tools, setTools] = useState<Tool[]>([]);
|
||||||
@@ -43,8 +187,7 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
const [toolHttpUrl, setToolHttpUrl] = useState('');
|
const [toolHttpUrl, setToolHttpUrl] = useState('');
|
||||||
const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}');
|
const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}');
|
||||||
const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000);
|
const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000);
|
||||||
const [toolParameterSchemaText, setToolParameterSchemaText] = useState(DEFAULT_PARAMETER_SCHEMA_TEXT);
|
const [toolParameters, setToolParameters] = useState<ToolParameterDraft[]>([]);
|
||||||
const [toolParameterDefaultsText, setToolParameterDefaultsText] = useState('{}');
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const loadTools = async () => {
|
const loadTools = async () => {
|
||||||
@@ -74,8 +217,7 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
setToolHttpUrl('');
|
setToolHttpUrl('');
|
||||||
setToolHttpHeadersText('{}');
|
setToolHttpHeadersText('{}');
|
||||||
setToolHttpTimeoutMs(10000);
|
setToolHttpTimeoutMs(10000);
|
||||||
setToolParameterSchemaText(DEFAULT_PARAMETER_SCHEMA_TEXT);
|
setToolParameters([]);
|
||||||
setToolParameterDefaultsText('{}');
|
|
||||||
setIsToolModalOpen(true);
|
setIsToolModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,8 +232,7 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
setToolHttpUrl(tool.httpUrl || '');
|
setToolHttpUrl(tool.httpUrl || '');
|
||||||
setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2));
|
setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2));
|
||||||
setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000);
|
setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000);
|
||||||
setToolParameterSchemaText(JSON.stringify(tool.parameterSchema || { type: 'object', properties: {}, required: [] }, null, 2));
|
setToolParameters(draftsFromSchema(tool.parameterSchema, tool.parameterDefaults));
|
||||||
setToolParameterDefaultsText(JSON.stringify(tool.parameterDefaults || {}, null, 2));
|
|
||||||
setIsToolModalOpen(true);
|
setIsToolModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,6 +297,18 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addToolParameter = () => {
|
||||||
|
setToolParameters((prev) => [...prev, newParameterDraft()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateToolParameter = (parameterId: string, patch: Partial<ToolParameterDraft>) => {
|
||||||
|
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 () => {
|
const handleSaveTool = async () => {
|
||||||
if (!toolName.trim()) {
|
if (!toolName.trim()) {
|
||||||
alert('请填写工具名称');
|
alert('请填写工具名称');
|
||||||
@@ -165,44 +318,14 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
let parsedHeaders: Record<string, string> = {};
|
let parsedHeaders: Record<string, string> = {};
|
||||||
let parsedParameterSchema: Record<string, any> = {};
|
const parameterConfig = buildToolParameterConfig(toolParameters);
|
||||||
let parsedParameterDefaults: Record<string, any> = {};
|
if (parameterConfig.error) {
|
||||||
|
alert(parameterConfig.error);
|
||||||
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<string, any>;
|
|
||||||
} 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<string, any>;
|
|
||||||
} catch {
|
|
||||||
alert('参数默认值必须是合法 JSON 对象');
|
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const parsedParameterSchema = parameterConfig.schema;
|
||||||
|
const parsedParameterDefaults = parameterConfig.defaults;
|
||||||
|
|
||||||
if (toolCategory === 'query') {
|
if (toolCategory === 'query') {
|
||||||
if (
|
if (
|
||||||
@@ -370,7 +493,7 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 max-h-[68vh] overflow-y-auto pr-1">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具类型</label>
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具类型</label>
|
||||||
<div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
|
<div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
|
||||||
@@ -429,25 +552,88 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 rounded-md border border-white/10 bg-white/5 p-3">
|
<div className="space-y-4 rounded-md border border-white/10 bg-white/5 p-3">
|
||||||
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-300">Tool Parameters</div>
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-1.5">
|
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-300">Tool Parameters</div>
|
||||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Schema (JSON Schema)</label>
|
<Button type="button" variant="outline" size="sm" onClick={addToolParameter}>
|
||||||
<textarea
|
<Plus className="w-3.5 h-3.5 mr-1" /> 添加参数
|
||||||
className="flex min-h-[110px] w-full rounded-md border border-white/10 bg-black/20 px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white font-mono"
|
</Button>
|
||||||
value={toolParameterSchemaText}
|
|
||||||
onChange={(e) => setToolParameterSchemaText(e.target.value)}
|
|
||||||
placeholder={DEFAULT_PARAMETER_SCHEMA_TEXT}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Default Args (JSON)</label>
|
|
||||||
<textarea
|
|
||||||
className="flex min-h-[90px] w-full rounded-md border border-white/10 bg-black/20 px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white font-mono"
|
|
||||||
value={toolParameterDefaultsText}
|
|
||||||
onChange={(e) => setToolParameterDefaultsText(e.target.value)}
|
|
||||||
placeholder='{"step": 1}'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{toolParameters.length === 0 ? (
|
||||||
|
<div className="rounded-md border border-white/10 bg-black/20 p-3 text-xs text-muted-foreground">
|
||||||
|
当前无参数。可以按 ElevenLabs 风格逐个添加参数,无需直接编辑 JSON Schema。
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{toolParameters.map((param) => (
|
||||||
|
<div key={param.id} className="rounded-md border border-white/10 bg-black/20 p-3 space-y-3">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-2">
|
||||||
|
<div className="md:col-span-2 space-y-1.5">
|
||||||
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Identifier</label>
|
||||||
|
<Input
|
||||||
|
value={param.key}
|
||||||
|
onChange={(e) => updateToolParameter(param.id, { key: e.target.value })}
|
||||||
|
placeholder="city"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Type</label>
|
||||||
|
<select
|
||||||
|
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
||||||
|
value={param.type}
|
||||||
|
onChange={(e) => updateToolParameter(param.id, { type: e.target.value as ToolParameterType })}
|
||||||
|
>
|
||||||
|
{TOOL_PARAMETER_TYPES.map((type) => (
|
||||||
|
<option key={type} value={type}>{type}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-xs text-muted-foreground md:mt-7">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={param.required}
|
||||||
|
onChange={(e) => updateToolParameter(param.id, { required: e.target.checked })}
|
||||||
|
/>
|
||||||
|
必填
|
||||||
|
</label>
|
||||||
|
<div className="flex items-end justify-end md:mt-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeToolParameter(param.id)}
|
||||||
|
className="h-9 px-3 rounded-md border border-white/10 text-xs text-muted-foreground hover:text-destructive hover:border-destructive/40"
|
||||||
|
>
|
||||||
|
删除参数
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Description</label>
|
||||||
|
<Input
|
||||||
|
value={param.description}
|
||||||
|
onChange={(e) => updateToolParameter(param.id, { description: e.target.value })}
|
||||||
|
placeholder="Parameter description for model"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={param.hasDefault}
|
||||||
|
onChange={(e) => updateToolParameter(param.id, { hasDefault: e.target.checked })}
|
||||||
|
/>
|
||||||
|
设置默认值
|
||||||
|
</label>
|
||||||
|
{param.hasDefault && (
|
||||||
|
<Input
|
||||||
|
value={param.defaultValueText}
|
||||||
|
onChange={(e) => updateToolParameter(param.id, { defaultValueText: e.target.value })}
|
||||||
|
placeholder={param.type === 'object' || param.type === 'array' ? 'JSON value' : 'Default value'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
支持系统指令和信息查询两类工具。Default Args 会在模型未传值时自动补齐。
|
支持系统指令和信息查询两类工具。Default Args 会在模型未传值时自动补齐。
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user