diff --git a/web/src/components/Settings/ConfigPage.tsx b/web/src/components/Settings/ConfigPage.tsx index 73117d8..cccad71 100644 --- a/web/src/components/Settings/ConfigPage.tsx +++ b/web/src/components/Settings/ConfigPage.tsx @@ -18,7 +18,7 @@ import type { SchedulerConfig, ChannelConfig, } from './types' import { TABS, inputCls, selectCls, TIMEZONE_OPTIONS } from './constants' -import { Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader, CheckboxList } from './ui' +import { Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader, CheckboxList, ModalHeader, ModalFooter } from './ui' import { getAppConfig, updateAppConfig, restartGateway, checkHealth } from '../../api/config' import { listSkills, toggleSkill } from '../../api/skills' import { listTools } from '../../api/tools' @@ -67,6 +67,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage deniedSkills: string[] allowedTools: string[] deniedTools: string[] + allowedSubagents: string[] + deniedSubagents: string[] } | null>(null) const [editingExpertError, setEditingExpertError] = useState('') const [savingExpert, setSavingExpert] = useState(false) @@ -77,6 +79,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage deniedSkills: string[] allowedTools: string[] deniedTools: string[] + allowedSubagents: string[] + deniedSubagents: string[] } | null>(null) const [editingSubagentError, setEditingSubagentError] = useState('') const [savingSubagent, setSavingSubagent] = useState(false) @@ -179,6 +183,11 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage } }, [activeTab, fetchSkillList, fetchToolList, skillList, toolList]) + // experts tab 编辑专家时也需要子代理勾选列表,按需加载(subagents tab 由下方独立 useEffect 刷新) + useEffect(() => { + if (activeTab === 'experts' && !subagentList) fetchSubagentList() + }, [activeTab, fetchSubagentList, subagentList]) + // Fetch expert list when experts tab is selected useEffect(() => { if (activeTab === 'experts') fetchExpertList() @@ -565,6 +574,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
启用 Task 工具 update('tools', { ...config.tools, task: { ...config.tools.task, enabled: v } })} />
update('tools', { ...config.tools, task: { ...config.tools.task, max_execution_secs: +e.target.value } })} className={inputCls} /> update('tools', { ...config.tools, task: { ...config.tools.task, ttl_hours: +e.target.value } })} className={inputCls} /> + update('tools', { ...config.tools, task: { ...config.tools.task, max_nesting_depth: Math.max(0, +e.target.value || 0) } })} className={inputCls} /> { setEditingExpertError('') - setEditingExpert({ mode: 'create', scope: 'project', nameField: '', description: '', body: '', allowedSkills: [], deniedSkills: [], allowedTools: [], deniedTools: [] }) + setEditingExpert({ mode: 'create', scope: 'project', nameField: '', description: '', body: '', allowedSkills: [], deniedSkills: [], allowedTools: [], deniedTools: [], allowedSubagents: [], deniedSubagents: [] }) }} className="flex items-center gap-2 px-4 py-2.5 rounded-xl border border-dashed border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:border-[var(--accent-cyan)]/30 transition-colors text-sm w-full justify-center" > @@ -815,6 +827,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage deniedSkills: expert.capability?.denied_skills ?? [], allowedTools: expert.capability?.allowed_tools ?? [], deniedTools: expert.capability?.denied_tools ?? [], + allowedSubagents: expert.capability?.allowed_subagents ?? [], + deniedSubagents: expert.capability?.denied_subagents ?? [], }) } @@ -885,8 +899,25 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage // 技能/工具勾选选项(专家与子代理编辑模态框共用) const skillOptions = (skillList?.skills ?? []).map(s => ({ key: s.name, label: s.name, description: s.description, group: s.source })) const toolOptions = (toolList?.tools ?? []).map(t => ({ key: t.name, label: t.name, description: t.description, group: t.source })) + const subagentOptions = (subagentList?.subagents ?? []).map(s => ({ key: s.name, label: s.name, description: s.description, group: s.source })) const skillEmptyHint = skillListLoading ? '加载中...' : '未发现任何技能,请先在技能页配置来源目录' const toolEmptyHint = toolListLoading ? '加载中...' : '未发现任何工具' + const subagentEmptyHint = subagentListLoading ? '加载中...' : '未发现任何子代理' + + // 子模态框打开时,ESC 键仅关闭子模态框(阻止冒泡到 ConfigPage 全局 ESC,避免关闭整个配置页) + useEffect(() => { + if (!editingExpert && !editingSubagent) return + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.stopPropagation() + e.preventDefault() + setEditingExpert(null) + setEditingSubagent(null) + } + } + window.addEventListener('keydown', handler, true) + return () => window.removeEventListener('keydown', handler, true) + }, [editingExpert, editingSubagent]) const renderExpertModal = () => { if (!editingExpert) return null @@ -898,14 +929,16 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage setSavingExpert(true) setEditingExpertError('') try { - // allowed_skills/allowed_tools 为空时必须传 undefined(后端 None=不限), + // allowed_* 为空时必须传 undefined(后端 None=不限), // 否则空数组会被反序列化为 Some(vec![]) 触发白名单空集语义(全禁)。 - // denied_skills/denied_tools 为 Vec,空数组即"不禁",可直接传。 + // denied_* 为 Vec,空数组即"不禁",可直接传。 const capability: CapabilityPolicy = { allowed_skills: editingExpert.allowedSkills.length > 0 ? editingExpert.allowedSkills : undefined, denied_skills: editingExpert.deniedSkills, allowed_tools: editingExpert.allowedTools.length > 0 ? editingExpert.allowedTools : undefined, denied_tools: editingExpert.deniedTools, + allowed_subagents: editingExpert.allowedSubagents.length > 0 ? editingExpert.allowedSubagents : undefined, + denied_subagents: editingExpert.deniedSubagents, } const payload = { name: editingExpert.nameField, @@ -934,90 +967,124 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage } return ( -
-
-
- -

- {isEdit ? '编辑专家' : '添加专家'} -

-
-
- - setEditingExpert(prev => prev ? { ...prev, nameField: e.target.value } : prev)} - disabled={isEdit} - placeholder="如 translator" - className={inputCls + (isEdit ? ' opacity-60 cursor-not-allowed' : '')} - autoFocus={!isEdit} - /> - - - setEditingExpert(prev => prev ? { ...prev, description: e.target.value } : prev)} - placeholder="如 翻译专家" - className={inputCls} - /> - - -