feat(web): 子代理模态框改造并暴露 max_nesting_depth 配置项
对抗审查修复 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 子代理选项排除自身避免意外自递归
This commit is contained in:
parent
c27efedb6c
commit
951e31aca4
@ -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
|
||||
<div className="flex items-center justify-between"><span className="text-sm text-[var(--text-secondary)]">启用 Task 工具</span><Toggle checked={config.tools.task.enabled} onChange={v => update('tools', { ...config.tools, task: { ...config.tools.task, enabled: v } })} /></div>
|
||||
<Field label="最大执行时间 (秒)"><input type="number" value={config.tools.task.max_execution_secs} onChange={e => update('tools', { ...config.tools, task: { ...config.tools.task, max_execution_secs: +e.target.value } })} className={inputCls} /></Field>
|
||||
<Field label="TTL (小时)"><input type="number" value={config.tools.task.ttl_hours} onChange={e => update('tools', { ...config.tools, task: { ...config.tools.task, ttl_hours: +e.target.value } })} className={inputCls} /></Field>
|
||||
<Field label="最大嵌套深度" hint="允许的子代理最大嵌套层数。1=仅子代理,2=子代理+孙代理(默认),0=禁止嵌套。深度达上限时移除 task 工具以防无限递归"><input type="number" min={0} value={config.tools.task.max_nesting_depth} onChange={e => update('tools', { ...config.tools, task: { ...config.tools.task, max_nesting_depth: Math.max(0, +e.target.value || 0) } })} className={inputCls} /></Field>
|
||||
</SectionCard>
|
||||
<SectionCard title="允许的工具列表">
|
||||
<SourceEditor
|
||||
@ -684,6 +694,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
deniedSkills: subagent.capability?.denied_skills ?? [],
|
||||
allowedTools: subagent.capability?.allowed_tools ?? [],
|
||||
deniedTools: subagent.capability?.denied_tools ?? [],
|
||||
allowedSubagents: subagent.capability?.allowed_subagents ?? [],
|
||||
deniedSubagents: subagent.capability?.denied_subagents ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
@ -752,7 +764,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
<button
|
||||
onClick={() => {
|
||||
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<String>,空数组即"不禁",可直接传。
|
||||
// denied_* 为 Vec<String>,空数组即"不禁",可直接传。
|
||||
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 (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl">
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-6 w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] overflow-y-auto">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<UserCheck className="h-5 w-5 text-[var(--accent-cyan)]" />
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
{isEdit ? '编辑专家' : '添加专家'}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Field label="名称" hint="创建后不可修改。仅当正文为空时,与描述一起生成兜底提示词;正文非空时不注入">
|
||||
<input
|
||||
value={editingExpert.nameField}
|
||||
onChange={e => setEditingExpert(prev => prev ? { ...prev, nameField: e.target.value } : prev)}
|
||||
disabled={isEdit}
|
||||
placeholder="如 translator"
|
||||
className={inputCls + (isEdit ? ' opacity-60 cursor-not-allowed' : '')}
|
||||
autoFocus={!isEdit}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="描述" hint="必填。仅当正文为空时,与名称一起生成兜底提示词;正文非空时不注入">
|
||||
<input
|
||||
value={editingExpert.description}
|
||||
onChange={e => setEditingExpert(prev => prev ? { ...prev, description: e.target.value } : prev)}
|
||||
placeholder="如 翻译专家"
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="专家提示词正文" hint="markdown 格式。非空时仅注入正文(不注入名称和描述);为空时自动用“名称+描述”生成兜底提示词">
|
||||
<textarea
|
||||
value={editingExpert.body}
|
||||
onChange={e => setEditingExpert(prev => prev ? { ...prev, body: e.target.value } : prev)}
|
||||
placeholder="你是一名专业翻译..."
|
||||
className={inputCls + ' min-h-[200px] resize-y font-mono text-xs'}
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2 border-t border-[var(--border-color)]">
|
||||
<Field label="允许的技能(白名单)" hint="留空表示不限。仅勾选的 SKILL.md 技能可见">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingExpert.allowedSkills}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, allowedSkills: v } : prev)}
|
||||
extraSelected={editingExpert.allowedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl" onClick={() => setEditingExpert(null)}>
|
||||
<div
|
||||
className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] flex flex-col overflow-hidden"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ModalHeader
|
||||
icon={<UserCheck className="h-5 w-5 text-[var(--accent-cyan)]" />}
|
||||
title={isEdit ? '编辑专家' : '添加专家'}
|
||||
onClose={() => setEditingExpert(null)}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
<SectionCard title="基本信息">
|
||||
<Field label="名称" hint="创建后不可修改。仅当正文为空时,与描述一起生成兜底提示词;正文非空时不注入">
|
||||
<input
|
||||
value={editingExpert.nameField}
|
||||
onChange={e => setEditingExpert(prev => prev ? { ...prev, nameField: e.target.value } : prev)}
|
||||
disabled={isEdit}
|
||||
placeholder="如 translator"
|
||||
className={inputCls + (isEdit ? ' opacity-60 cursor-not-allowed' : '')}
|
||||
autoFocus={!isEdit}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的技能(黑名单)" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingExpert.deniedSkills}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, deniedSkills: v } : prev)}
|
||||
extraSelected={editingExpert.deniedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
<Field label="描述" hint="必填。仅当正文为空时,与名称一起生成兜底提示词;正文非空时不注入">
|
||||
<input
|
||||
value={editingExpert.description}
|
||||
onChange={e => setEditingExpert(prev => prev ? { ...prev, description: e.target.value } : prev)}
|
||||
placeholder="如 翻译专家"
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="允许的工具(白名单)" hint="留空表示不限。覆盖内置 + MCP 工具">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingExpert.allowedTools}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, allowedTools: v } : prev)}
|
||||
extraSelected={editingExpert.allowedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
<Field label="专家提示词正文" hint="markdown 格式。非空时仅注入正文;为空时自动用“名称+描述”生成兜底提示词">
|
||||
<textarea
|
||||
value={editingExpert.body}
|
||||
onChange={e => setEditingExpert(prev => prev ? { ...prev, body: e.target.value } : prev)}
|
||||
placeholder="你是一名专业翻译..."
|
||||
className={inputCls + ' min-h-[160px] resize-y font-mono text-xs'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的工具" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingExpert.deniedTools}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, deniedTools: v } : prev)}
|
||||
extraSelected={editingExpert.deniedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard title="技能能力" subtitle="白名单取交集,黑名单扣除">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Field label="允许的技能(白名单)" hint="留空表示不限">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingExpert.allowedSkills}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, allowedSkills: v } : prev)}
|
||||
extraSelected={editingExpert.allowedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的技能(黑名单)" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingExpert.deniedSkills}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, deniedSkills: v } : prev)}
|
||||
extraSelected={editingExpert.deniedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard title="工具能力" subtitle="含 mcp_*">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Field label="允许的工具(白名单)" hint="留空表示不限">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingExpert.allowedTools}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, allowedTools: v } : prev)}
|
||||
extraSelected={editingExpert.allowedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的工具" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingExpert.deniedTools}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, deniedTools: v } : prev)}
|
||||
extraSelected={editingExpert.deniedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard title="子代理能力" subtitle="限制可加载的子代理">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Field label="允许的子代理(白名单)" hint="留空表示不限。仅勾选的子代理可被加载">
|
||||
<CheckboxList
|
||||
options={subagentOptions}
|
||||
selected={editingExpert.allowedSubagents}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, allowedSubagents: v } : prev)}
|
||||
extraSelected={editingExpert.allowedSubagents}
|
||||
emptyHint={subagentEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的子代理(黑名单)" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={subagentOptions}
|
||||
selected={editingExpert.deniedSubagents}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, deniedSubagents: v } : prev)}
|
||||
extraSelected={editingExpert.deniedSubagents}
|
||||
emptyHint={subagentEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
{editingExpertError && (
|
||||
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||
{editingExpertError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end mt-5">
|
||||
<ModalFooter>
|
||||
<button
|
||||
onClick={() => setEditingExpert(null)}
|
||||
className="px-4 py-2 rounded-lg text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
@ -1032,7 +1099,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
{savingExpert ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||
{savingExpert ? '保存中...' : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -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 (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl">
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-6 w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] overflow-y-auto">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Bot className="h-5 w-5 text-[var(--accent-cyan)]" />
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">编辑子代理</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Field label="名称">
|
||||
<input
|
||||
value={editingSubagent.name}
|
||||
disabled
|
||||
className={inputCls + ' opacity-60 cursor-not-allowed'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="描述" hint="子代理的简短描述,用于主智能体选择">
|
||||
<input
|
||||
value={editingSubagent.description}
|
||||
onChange={e => setEditingSubagent(prev => prev ? { ...prev, description: e.target.value } : prev)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2 border-t border-[var(--border-color)]">
|
||||
<Field label="允许的技能(白名单)" hint="留空表示不限。仅勾选的 SKILL.md 技能可见">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingSubagent.allowedSkills}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, allowedSkills: v } : prev)}
|
||||
extraSelected={editingSubagent.allowedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl" onClick={() => setEditingSubagent(null)}>
|
||||
<div
|
||||
className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] flex flex-col overflow-hidden"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ModalHeader
|
||||
icon={<Bot className="h-5 w-5 text-[var(--accent-cyan)]" />}
|
||||
title="编辑子代理"
|
||||
onClose={() => setEditingSubagent(null)}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
<SectionCard title="基本信息">
|
||||
<Field label="名称">
|
||||
<input
|
||||
value={editingSubagent.name}
|
||||
disabled
|
||||
className={inputCls + ' opacity-60 cursor-not-allowed'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的技能(黑名单)" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingSubagent.deniedSkills}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, deniedSkills: v } : prev)}
|
||||
extraSelected={editingSubagent.deniedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
<Field label="描述" hint="子代理的简短描述,用于主智能体选择">
|
||||
<input
|
||||
value={editingSubagent.description}
|
||||
onChange={e => setEditingSubagent(prev => prev ? { ...prev, description: e.target.value } : prev)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="允许的工具(白名单)" hint="留空表示不限。覆盖内置 + MCP 工具">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingSubagent.allowedTools}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, allowedTools: v } : prev)}
|
||||
extraSelected={editingSubagent.allowedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的工具" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingSubagent.deniedTools}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, deniedTools: v } : prev)}
|
||||
extraSelected={editingSubagent.deniedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard title="技能能力" subtitle="白名单取交集,黑名单扣除">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Field label="允许的技能(白名单)" hint="留空表示不限。仅勾选的 SKILL.md 技能可见">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingSubagent.allowedSkills}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, allowedSkills: v } : prev)}
|
||||
extraSelected={editingSubagent.allowedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的技能(黑名单)" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingSubagent.deniedSkills}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, deniedSkills: v } : prev)}
|
||||
extraSelected={editingSubagent.deniedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard title="工具能力" subtitle="含 mcp_*">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Field label="允许的工具(白名单)" hint="留空表示不限。覆盖内置 + MCP 工具">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingSubagent.allowedTools}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, allowedTools: v } : prev)}
|
||||
extraSelected={editingSubagent.allowedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的工具" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingSubagent.deniedTools}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, deniedTools: v } : prev)}
|
||||
extraSelected={editingSubagent.deniedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard title="子代理能力" subtitle="限制可加载的孙代理">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Field label="允许的子代理(白名单)" hint="留空表示不限。仅勾选的子代理可被加载为孙代理">
|
||||
<CheckboxList
|
||||
options={subagentOptionsExcludingSelf}
|
||||
selected={editingSubagent.allowedSubagents}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, allowedSubagents: v } : prev)}
|
||||
extraSelected={editingSubagent.allowedSubagents}
|
||||
emptyHint={subagentEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的子代理(黑名单)" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={subagentOptionsExcludingSelf}
|
||||
selected={editingSubagent.deniedSubagents}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, deniedSubagents: v } : prev)}
|
||||
extraSelected={editingSubagent.deniedSubagents}
|
||||
emptyHint={subagentEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
{editingSubagentError && (
|
||||
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||
{editingSubagentError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end mt-5">
|
||||
<ModalFooter>
|
||||
<button
|
||||
onClick={() => setEditingSubagent(null)}
|
||||
className="px-4 py-2 rounded-lg text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
@ -1161,7 +1270,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
{savingSubagent ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||
{savingSubagent ? '保存中...' : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 (
|
||||
<div className="flex items-center gap-3 shrink-0 px-6 py-4 border-b border-[var(--border-color)] bg-[var(--bg-tertiary)]/50">
|
||||
{icon}
|
||||
<span className="text-base font-semibold text-[var(--text-primary)]">{title}</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="ml-auto p-2 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
aria-label="关闭"
|
||||
title="关闭 (Esc)"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 模态框底部按钮区,与项目模态框惯例一致 */
|
||||
export function ModalFooter({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="shrink-0 px-6 py-3 border-t border-[var(--border-color)] bg-[var(--bg-tertiary)]/30 flex items-center gap-3 justify-end">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SourceEditor({
|
||||
sources,
|
||||
onChange,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user