95 lines
7.6 KiB
TypeScript
95 lines
7.6 KiB
TypeScript
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>;
|
||
}
|