Files
makelore/src/pages/CloudAgents/ConfigurationFields.tsx

95 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react';
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { CloudKnowledgePanel } from './CloudKnowledge';
import { CloudResources } from './CloudResources';
import { CloudBudgetEditor } from './CloudCosts';
import type { CloudAgentConfiguration, CloudCatalog } from '../../../shared/cloud-agents';
export function ConfigurationFields({ slug, value, disabled, onChange, section = 'all' }: {
section?: 'all' | 'instructions' | 'capabilities' | 'knowledge' | 'limits';
slug: string; value: CloudAgentConfiguration; disabled: boolean; onChange: (value: CloudAgentConfiguration) => void;
}) {
const [catalog, setCatalog] = useState<CloudCatalog | null>(null);
const [error, setError] = useState('');
const [refresh, setRefresh] = useState(0);
const [search, setSearch] = useState('');
const [choosing, setChoosing] = useState<string | null>(null);
useEffect(() => {
let live = true;
cloudAgentsApi.call('catalog', {}).then(v => { if (live) { setCatalog(v); setError(''); } })
.catch(e => { if (live) setError(e instanceof Error ? e.message : '配置目录暂不可用'); });
return () => { live = false; };
}, [refresh]);
const change = <K extends keyof CloudAgentConfiguration>(key: K, next: CloudAgentConfiguration[K]) => onChange({ ...value, [key]: next });
return <div className="space-y-6">
<div hidden={section === 'instructions' || section === 'limits'} className="flex items-center justify-between"><h2 className="font-medium">{section === 'knowledge' ? '知识库' : '能力'}</h2>
<Button type="button" variant="ghost" disabled={disabled} onClick={() => setRefresh(v => v + 1)}>刷新目录</Button></div>
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}
<div hidden={section !== 'all' && section !== 'capabilities'} className="space-y-4"><label className="block space-y-2 text-sm"><span>模型</span>
<select aria-label="模型" className="h-10 w-full rounded-md border border-input bg-background px-3" disabled={disabled || !catalog}
value={value.model} onChange={e => change('model', e.target.value)}>
<option value="">选择可用模型</option>
{value.model && !catalog?.models.some(m => m.id === value.model) && <option value={value.model}>{value.model}(目录中不可用)</option>}
{catalog?.models.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
</select></label>
{catalog?.pricing && <p className="text-xs leading-5 text-muted-foreground tabular-nums">
每 {catalog.pricing.unit_size.toLocaleString()} 个词元约 {catalog.pricing.points_per_unit} 词元点数,按实际模型调用结算。
</p>}
</div>
{(['tools', 'knowledges', 'mcps', 'skills', 'subagents'] as const).map(key => {
const labels = { tools: '工具', knowledges: '知识库', mcps: 'MCP 服务', skills: 'Skills', subagents: '子智能体' };
const options = catalog?.resources[key] ?? [];
const missing = value[key].filter(id => !options.some(option => option.key === id));
return <fieldset key={key} disabled={disabled} hidden={section !== 'all' && section !== (key === 'knowledges' ? 'knowledge' : 'capabilities')} className="space-y-2">
<legend className="mb-2 text-sm font-medium">{labels[key]} <span className="font-normal text-muted-foreground">· 已选 {value[key].length}</span></legend>
{!options.length && !missing.length && <p className="text-xs text-muted-foreground">{catalog ? '当前没有可用资源' : '正在读取资源…'}</p>}
<Button variant="outline" size="sm" onClick={() => { setChoosing(choosing === key ? null : key); setSearch(''); }}>{choosing === key ? '完成选择' : '选择' + labels[key]}</Button>
{choosing === key && <Input aria-label={'搜索' + labels[key]} placeholder={'搜索' + labels[key]} value={search} onChange={e => setSearch(e.target.value)} />}
<div className="max-h-64 overflow-auto rounded-lg bg-muted/30">
{[...options, ...missing.map(id => ({ key: id, name: id + '(已不可用)', description: '取消选择后重新保存' }))].filter(option => choosing === key ? (option.name + ' ' + (option.description ?? '')).toLowerCase().includes(search.toLowerCase()) : value[key].includes(option.key)).map(option =>
<label key={option.key} className="flex min-h-10 cursor-pointer items-start gap-3 px-3 py-2 text-sm hover:bg-muted/50">
<input className="mt-1 h-4 w-4 accent-primary" type="checkbox" checked={value[key].includes(option.key)}
onChange={e => {
const selected = e.target.checked ? [...value[key], option.key] : value[key].filter(id => id !== option.key);
onChange({ ...value, [key]: selected, ...(key === 'skills' ? { preload_skills: value.preload_skills.filter(id => selected.includes(id)) } : {}) });
}} />
<span><span>{option.name}</span>{option.description && <span className="mt-1 block text-xs leading-5 text-muted-foreground">{option.description}</span>}</span>
</label>)}
</div>
{key === 'skills' && value.skills.length > 0 && <label className="flex min-h-10 items-center gap-2 text-xs">
<input type="checkbox" checked={value.preload_skills.length === value.skills.length}
onChange={e => change('preload_skills', e.target.checked ? [...value.skills] : [])} />开始运行时预加载已选 Skills</label>}
</fieldset>;
})}
<div hidden={section !== 'all' && section !== 'capabilities'} className="space-y-4">
<p className="text-xs leading-5 text-muted-foreground">未选择的资源不会开放给此智能体。分享使用时仍按本配置限制能力。</p>
<CloudResources onChanged={() => setRefresh(v => v + 1)} />
</div>
<div hidden={section !== 'all' && section !== 'knowledge'}>
<CloudKnowledgePanel slug={slug} onDeleted={id => { change('knowledges', value.knowledges.filter(key => key !== id)); setRefresh(v => v + 1); }} onCreated={kb => {
change('knowledges', [...new Set([...value.knowledges, kb.kb_id])]);
setRefresh(v => v + 1);
}} />
</div>
<div hidden={section !== 'all' && section !== 'limits'} className="space-y-6">
<CloudBudgetEditor slug={slug} />
<h2 className="font-medium">运行限制</h2><p className="text-xs text-muted-foreground">以下设置随草稿保存;费用上限独立保存,对后续调用生效。</p>
<label className="block space-y-2 text-sm"><span>工具审批</span>
<select className="h-10 w-full rounded-md border border-input bg-background px-3" disabled={disabled}
value={value.tool_approval_mode} onChange={e => change('tool_approval_mode', e.target.value === 'always_trust' ? 'always_trust' : 'default')}>
<option value="default">按工具要求确认</option><option value="always_trust">信任已选工具,自动执行</option>
</select></label>
<div className="grid gap-4">
{([{ key: 'max_execution_steps', label: '最多执行步数', min: 1, max: 300 },
{ key: 'max_output_tokens', label: '单次输出词元上限', min: 1, max: 32768 },
{ key: 'max_run_seconds', label: '单次运行时限(秒)', min: 10, max: 3600 }] as const).map(item =>
<label key={item.key} className="space-y-2 text-xs"><span>{item.label}</span>
<Input type="number" min={item.min} max={item.max} value={value[item.key]} disabled={disabled}
onChange={e => change(item.key, Number(e.target.value))} /></label>)}
</div>
</div>
</div>;
}