From 951e31aca4c45f8ff1083029a25f48e200cd4873 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Thu, 30 Jul 2026 17:38:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=AD=90=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E6=A1=86=E6=94=B9=E9=80=A0=E5=B9=B6=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2=20max=5Fnesting=5Fdepth=20=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对抗审查修复 4 个 P0 问题: - 子代理 modal handleSave 缺失 allowed_subagents/denied_subagents 字段,导致保存时既有策略被后端 #[serde(default)] 静默清空(数据丢失) - 子代理 modal 缺失子代理黑白名单 CheckboxList UI - 子代理 modal 未改造为 ModalHeader/ModalFooter/SectionCard,无 X 关闭按钮与遮罩 onClick - experts tab 未加载 subagentList,编辑专家时子代理勾选列表显示空 新增改动: - ui.tsx 新增 ModalHeader/ModalFooter 通用组件 - ConfigPage renderTools 新增最大嵌套深度输入框(后端 TaskConfig.max_nesting_depth 早已存在,默认 2) - 子代理 modal 子代理选项排除自身避免意外自递归 --- web/src/components/Settings/ConfigPage.tsx | 385 +++++++++++++-------- web/src/components/Settings/types.ts | 4 +- web/src/components/Settings/ui.tsx | 27 ++ 3 files changed, 277 insertions(+), 139 deletions(-) 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} - /> - - -