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}
- />
-
-
-
-
-
- setEditingExpert(prev => prev ? { ...prev, allowedSkills: v } : prev)}
- extraSelected={editingExpert.allowedSkills}
- emptyHint={skillEmptyHint}
- groupBy={o => o.group ?? '其他'}
+ setEditingExpert(null)}>
+
e.stopPropagation()}
+ >
+
}
+ title={isEdit ? '编辑专家' : '添加专家'}
+ onClose={() => setEditingExpert(null)}
+ />
+
+
+
+ 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, deniedSkills: v } : prev)}
- extraSelected={editingExpert.deniedSkills}
- emptyHint={skillEmptyHint}
- groupBy={o => o.group ?? '其他'}
+
+ setEditingExpert(prev => prev ? { ...prev, description: e.target.value } : prev)}
+ placeholder="如 翻译专家"
+ className={inputCls}
/>
-
- setEditingExpert(prev => prev ? { ...prev, allowedTools: v } : prev)}
- extraSelected={editingExpert.allowedTools}
- emptyHint={toolEmptyHint}
- groupBy={o => o.group ?? '其他'}
+
+
-
- setEditingExpert(prev => prev ? { ...prev, deniedTools: v } : prev)}
- extraSelected={editingExpert.deniedTools}
- emptyHint={toolEmptyHint}
- groupBy={o => o.group ?? '其他'}
- />
-
-
+
+
+
+
+ setEditingExpert(prev => prev ? { ...prev, allowedSkills: v } : prev)}
+ extraSelected={editingExpert.allowedSkills}
+ emptyHint={skillEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+ setEditingExpert(prev => prev ? { ...prev, deniedSkills: v } : prev)}
+ extraSelected={editingExpert.deniedSkills}
+ emptyHint={skillEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+
+
+
+
+ setEditingExpert(prev => prev ? { ...prev, allowedTools: v } : prev)}
+ extraSelected={editingExpert.allowedTools}
+ emptyHint={toolEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+ setEditingExpert(prev => prev ? { ...prev, deniedTools: v } : prev)}
+ extraSelected={editingExpert.deniedTools}
+ emptyHint={toolEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+
+
+
+
+ setEditingExpert(prev => prev ? { ...prev, allowedSubagents: v } : prev)}
+ extraSelected={editingExpert.allowedSubagents}
+ emptyHint={subagentEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+ setEditingExpert(prev => prev ? { ...prev, deniedSubagents: v } : prev)}
+ extraSelected={editingExpert.deniedSubagents}
+ emptyHint={subagentEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+
{editingExpertError && (
{editingExpertError}
)}
-
+
-
+
)
@@ -1041,6 +1108,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
const renderSubagentModal = () => {
if (!editingSubagent) return null
const canSave = editingSubagent.description.trim().length > 0
+ // 排除自身,避免子代理勾选自己造成意外自递归(max_nesting_depth 仍兜底)
+ const subagentOptionsExcludingSelf = subagentOptions.filter(o => o.key !== editingSubagent.name)
const handleSave = async () => {
if (!canSave) return
@@ -1048,11 +1117,15 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
setEditingSubagentError('')
try {
// 与专家一致:allowed_* 为空时传 undefined(None=不限),denied_* 空数组即"不禁"
+ // 必须包含全部 6 个字段,否则后端 #[serde(default)] 会让缺失字段变为 None/vec![],
+ // 经 next_capability 完全覆盖原 capability,导致既有策略被清空(数据丢失)
const capability: CapabilityPolicy = {
allowed_skills: editingSubagent.allowedSkills.length > 0 ? editingSubagent.allowedSkills : undefined,
denied_skills: editingSubagent.deniedSkills,
allowed_tools: editingSubagent.allowedTools.length > 0 ? editingSubagent.allowedTools : undefined,
denied_tools: editingSubagent.deniedTools,
+ allowed_subagents: editingSubagent.allowedSubagents.length > 0 ? editingSubagent.allowedSubagents : undefined,
+ denied_subagents: editingSubagent.deniedSubagents,
}
const resp = await updateSubagentCb({
name: editingSubagent.name,
@@ -1077,76 +1150,112 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
}
return (
-
-
-
-
-
编辑子代理
-
-
-
-
-
-
- setEditingSubagent(prev => prev ? { ...prev, description: e.target.value } : prev)}
- className={inputCls}
- />
-
-
-
- setEditingSubagent(prev => prev ? { ...prev, allowedSkills: v } : prev)}
- extraSelected={editingSubagent.allowedSkills}
- emptyHint={skillEmptyHint}
- groupBy={o => o.group ?? '其他'}
+ setEditingSubagent(null)}>
+
e.stopPropagation()}
+ >
+
}
+ title="编辑子代理"
+ onClose={() => setEditingSubagent(null)}
+ />
+
+
+
+
-
- setEditingSubagent(prev => prev ? { ...prev, deniedSkills: v } : prev)}
- extraSelected={editingSubagent.deniedSkills}
- emptyHint={skillEmptyHint}
- groupBy={o => o.group ?? '其他'}
+
+ setEditingSubagent(prev => prev ? { ...prev, description: e.target.value } : prev)}
+ className={inputCls}
/>
-
- setEditingSubagent(prev => prev ? { ...prev, allowedTools: v } : prev)}
- extraSelected={editingSubagent.allowedTools}
- emptyHint={toolEmptyHint}
- groupBy={o => o.group ?? '其他'}
- />
-
-
- setEditingSubagent(prev => prev ? { ...prev, deniedTools: v } : prev)}
- extraSelected={editingSubagent.deniedTools}
- emptyHint={toolEmptyHint}
- groupBy={o => o.group ?? '其他'}
- />
-
-
+
+
+
+
+ setEditingSubagent(prev => prev ? { ...prev, allowedSkills: v } : prev)}
+ extraSelected={editingSubagent.allowedSkills}
+ emptyHint={skillEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+ setEditingSubagent(prev => prev ? { ...prev, deniedSkills: v } : prev)}
+ extraSelected={editingSubagent.deniedSkills}
+ emptyHint={skillEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+
+
+
+
+ setEditingSubagent(prev => prev ? { ...prev, allowedTools: v } : prev)}
+ extraSelected={editingSubagent.allowedTools}
+ emptyHint={toolEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+ setEditingSubagent(prev => prev ? { ...prev, deniedTools: v } : prev)}
+ extraSelected={editingSubagent.deniedTools}
+ emptyHint={toolEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+
+
+
+
+ setEditingSubagent(prev => prev ? { ...prev, allowedSubagents: v } : prev)}
+ extraSelected={editingSubagent.allowedSubagents}
+ emptyHint={subagentEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+ setEditingSubagent(prev => prev ? { ...prev, deniedSubagents: v } : prev)}
+ extraSelected={editingSubagent.deniedSubagents}
+ emptyHint={subagentEmptyHint}
+ groupBy={o => o.group ?? '其他'}
+ />
+
+
+
{editingSubagentError && (
{editingSubagentError}
)}
-
+
-
+
)
diff --git a/web/src/components/Settings/types.ts b/web/src/components/Settings/types.ts
index 481c4ea..f727f1c 100644
--- a/web/src/components/Settings/types.ts
+++ b/web/src/components/Settings/types.ts
@@ -7,7 +7,7 @@ export interface GatewayConfig { host: string; port: number; show_tool_results:
export interface TimeConfig { timezone: string }
export interface SchedulerConfig { enabled: boolean; tick_resolution_ms: number; worker_queue_capacity: number; misfire_policy: 'skip' | 'catch_up'; jobs?: SchedulerJobConfig[] }
export interface SkillsConfig { enabled: boolean; sources: string[]; max_index_chars: number; max_listed_skills: number }
-export interface TaskConfig { enabled: boolean; max_execution_secs: number; ttl_hours: number; allowed_tools: string[] }
+export interface TaskConfig { enabled: boolean; max_execution_secs: number; ttl_hours: number; allowed_tools: string[]; max_nesting_depth: number }
export interface ToolsConfig { disabled: string[]; task: TaskConfig }
export interface MemoryMaintenanceConfig { max_merge_ratio: number; min_memories_to_keep: number; max_merge_per_group: number }
export interface ImageContextConfig { max_images_in_context: number; max_image_age_rounds: number }
@@ -57,6 +57,8 @@ export interface CapabilityPolicy {
denied_skills: string[]
allowed_tools?: string[]
denied_tools: string[]
+ allowed_subagents?: string[]
+ denied_subagents: string[]
}
export interface SubagentItem {
diff --git a/web/src/components/Settings/ui.tsx b/web/src/components/Settings/ui.tsx
index 754dc36..6573b6c 100644
--- a/web/src/components/Settings/ui.tsx
+++ b/web/src/components/Settings/ui.tsx
@@ -63,6 +63,33 @@ export function SectionCard({ title, subtitle, children }: { title: string; subt
)
}
+/** 模态框标题栏:图标 + 标题 + X 关闭按钮,与项目模态框惯例一致 */
+export function ModalHeader({ icon, title, onClose }: { icon: ReactNode; title: string; onClose: () => void }) {
+ return (
+
+ {icon}
+ {title}
+
+
+ )
+}
+
+/** 模态框底部按钮区,与项目模态框惯例一致 */
+export function ModalFooter({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
export function SourceEditor({
sources,
onChange,