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,
|
SchedulerConfig, ChannelConfig,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { TABS, inputCls, selectCls, TIMEZONE_OPTIONS } from './constants'
|
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 { getAppConfig, updateAppConfig, restartGateway, checkHealth } from '../../api/config'
|
||||||
import { listSkills, toggleSkill } from '../../api/skills'
|
import { listSkills, toggleSkill } from '../../api/skills'
|
||||||
import { listTools } from '../../api/tools'
|
import { listTools } from '../../api/tools'
|
||||||
@ -67,6 +67,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
deniedSkills: string[]
|
deniedSkills: string[]
|
||||||
allowedTools: string[]
|
allowedTools: string[]
|
||||||
deniedTools: string[]
|
deniedTools: string[]
|
||||||
|
allowedSubagents: string[]
|
||||||
|
deniedSubagents: string[]
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [editingExpertError, setEditingExpertError] = useState('')
|
const [editingExpertError, setEditingExpertError] = useState('')
|
||||||
const [savingExpert, setSavingExpert] = useState(false)
|
const [savingExpert, setSavingExpert] = useState(false)
|
||||||
@ -77,6 +79,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
deniedSkills: string[]
|
deniedSkills: string[]
|
||||||
allowedTools: string[]
|
allowedTools: string[]
|
||||||
deniedTools: string[]
|
deniedTools: string[]
|
||||||
|
allowedSubagents: string[]
|
||||||
|
deniedSubagents: string[]
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [editingSubagentError, setEditingSubagentError] = useState('')
|
const [editingSubagentError, setEditingSubagentError] = useState('')
|
||||||
const [savingSubagent, setSavingSubagent] = useState(false)
|
const [savingSubagent, setSavingSubagent] = useState(false)
|
||||||
@ -179,6 +183,11 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
}
|
}
|
||||||
}, [activeTab, fetchSkillList, fetchToolList, skillList, toolList])
|
}, [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
|
// Fetch expert list when experts tab is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'experts') fetchExpertList()
|
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>
|
<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="最大执行时间 (秒)"><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="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>
|
||||||
<SectionCard title="允许的工具列表">
|
<SectionCard title="允许的工具列表">
|
||||||
<SourceEditor
|
<SourceEditor
|
||||||
@ -684,6 +694,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
deniedSkills: subagent.capability?.denied_skills ?? [],
|
deniedSkills: subagent.capability?.denied_skills ?? [],
|
||||||
allowedTools: subagent.capability?.allowed_tools ?? [],
|
allowedTools: subagent.capability?.allowed_tools ?? [],
|
||||||
deniedTools: subagent.capability?.denied_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
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingExpertError('')
|
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"
|
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 ?? [],
|
deniedSkills: expert.capability?.denied_skills ?? [],
|
||||||
allowedTools: expert.capability?.allowed_tools ?? [],
|
allowedTools: expert.capability?.allowed_tools ?? [],
|
||||||
deniedTools: expert.capability?.denied_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 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 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 skillEmptyHint = skillListLoading ? '加载中...' : '未发现任何技能,请先在技能页配置来源目录'
|
||||||
const toolEmptyHint = toolListLoading ? '加载中...' : '未发现任何工具'
|
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 = () => {
|
const renderExpertModal = () => {
|
||||||
if (!editingExpert) return null
|
if (!editingExpert) return null
|
||||||
@ -898,14 +929,16 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
setSavingExpert(true)
|
setSavingExpert(true)
|
||||||
setEditingExpertError('')
|
setEditingExpertError('')
|
||||||
try {
|
try {
|
||||||
// allowed_skills/allowed_tools 为空时必须传 undefined(后端 None=不限),
|
// allowed_* 为空时必须传 undefined(后端 None=不限),
|
||||||
// 否则空数组会被反序列化为 Some(vec![]) 触发白名单空集语义(全禁)。
|
// 否则空数组会被反序列化为 Some(vec![]) 触发白名单空集语义(全禁)。
|
||||||
// denied_skills/denied_tools 为 Vec<String>,空数组即"不禁",可直接传。
|
// denied_* 为 Vec<String>,空数组即"不禁",可直接传。
|
||||||
const capability: CapabilityPolicy = {
|
const capability: CapabilityPolicy = {
|
||||||
allowed_skills: editingExpert.allowedSkills.length > 0 ? editingExpert.allowedSkills : undefined,
|
allowed_skills: editingExpert.allowedSkills.length > 0 ? editingExpert.allowedSkills : undefined,
|
||||||
denied_skills: editingExpert.deniedSkills,
|
denied_skills: editingExpert.deniedSkills,
|
||||||
allowed_tools: editingExpert.allowedTools.length > 0 ? editingExpert.allowedTools : undefined,
|
allowed_tools: editingExpert.allowedTools.length > 0 ? editingExpert.allowedTools : undefined,
|
||||||
denied_tools: editingExpert.deniedTools,
|
denied_tools: editingExpert.deniedTools,
|
||||||
|
allowed_subagents: editingExpert.allowedSubagents.length > 0 ? editingExpert.allowedSubagents : undefined,
|
||||||
|
denied_subagents: editingExpert.deniedSubagents,
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
name: editingExpert.nameField,
|
name: editingExpert.nameField,
|
||||||
@ -934,15 +967,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl">
|
<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 p-6 w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] overflow-y-auto">
|
<div
|
||||||
<div className="flex items-center gap-2 mb-4">
|
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"
|
||||||
<UserCheck className="h-5 w-5 text-[var(--accent-cyan)]" />
|
onClick={e => e.stopPropagation()}
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">
|
>
|
||||||
{isEdit ? '编辑专家' : '添加专家'}
|
<ModalHeader
|
||||||
</h3>
|
icon={<UserCheck className="h-5 w-5 text-[var(--accent-cyan)]" />}
|
||||||
</div>
|
title={isEdit ? '编辑专家' : '添加专家'}
|
||||||
<div className="space-y-3">
|
onClose={() => setEditingExpert(null)}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||||
|
<SectionCard title="基本信息">
|
||||||
<Field label="名称" hint="创建后不可修改。仅当正文为空时,与描述一起生成兜底提示词;正文非空时不注入">
|
<Field label="名称" hint="创建后不可修改。仅当正文为空时,与描述一起生成兜底提示词;正文非空时不注入">
|
||||||
<input
|
<input
|
||||||
value={editingExpert.nameField}
|
value={editingExpert.nameField}
|
||||||
@ -961,16 +997,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="专家提示词正文" hint="markdown 格式。非空时仅注入正文(不注入名称和描述);为空时自动用“名称+描述”生成兜底提示词">
|
<Field label="专家提示词正文" hint="markdown 格式。非空时仅注入正文;为空时自动用“名称+描述”生成兜底提示词">
|
||||||
<textarea
|
<textarea
|
||||||
value={editingExpert.body}
|
value={editingExpert.body}
|
||||||
onChange={e => setEditingExpert(prev => prev ? { ...prev, body: e.target.value } : prev)}
|
onChange={e => setEditingExpert(prev => prev ? { ...prev, body: e.target.value } : prev)}
|
||||||
placeholder="你是一名专业翻译..."
|
placeholder="你是一名专业翻译..."
|
||||||
className={inputCls + ' min-h-[200px] resize-y font-mono text-xs'}
|
className={inputCls + ' min-h-[160px] resize-y font-mono text-xs'}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2 border-t border-[var(--border-color)]">
|
</SectionCard>
|
||||||
<Field label="允许的技能(白名单)" hint="留空表示不限。仅勾选的 SKILL.md 技能可见">
|
<SectionCard title="技能能力" subtitle="白名单取交集,黑名单扣除">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<Field label="允许的技能(白名单)" hint="留空表示不限">
|
||||||
<CheckboxList
|
<CheckboxList
|
||||||
options={skillOptions}
|
options={skillOptions}
|
||||||
selected={editingExpert.allowedSkills}
|
selected={editingExpert.allowedSkills}
|
||||||
@ -990,7 +1028,11 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
groupBy={o => o.group ?? '其他'}
|
groupBy={o => o.group ?? '其他'}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="允许的工具(白名单)" hint="留空表示不限。覆盖内置 + MCP 工具">
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
<SectionCard title="工具能力" subtitle="含 mcp_*">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<Field label="允许的工具(白名单)" hint="留空表示不限">
|
||||||
<CheckboxList
|
<CheckboxList
|
||||||
options={toolOptions}
|
options={toolOptions}
|
||||||
selected={editingExpert.allowedTools}
|
selected={editingExpert.allowedTools}
|
||||||
@ -1011,13 +1053,38 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</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 && (
|
{editingExpertError && (
|
||||||
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||||
{editingExpertError}
|
{editingExpertError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 justify-end mt-5">
|
<ModalFooter>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingExpert(null)}
|
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"
|
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 ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||||
{savingExpert ? '保存中...' : '保存'}
|
{savingExpert ? '保存中...' : '保存'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</ModalFooter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -1041,6 +1108,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
const renderSubagentModal = () => {
|
const renderSubagentModal = () => {
|
||||||
if (!editingSubagent) return null
|
if (!editingSubagent) return null
|
||||||
const canSave = editingSubagent.description.trim().length > 0
|
const canSave = editingSubagent.description.trim().length > 0
|
||||||
|
// 排除自身,避免子代理勾选自己造成意外自递归(max_nesting_depth 仍兜底)
|
||||||
|
const subagentOptionsExcludingSelf = subagentOptions.filter(o => o.key !== editingSubagent.name)
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!canSave) return
|
if (!canSave) return
|
||||||
@ -1048,11 +1117,15 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
setEditingSubagentError('')
|
setEditingSubagentError('')
|
||||||
try {
|
try {
|
||||||
// 与专家一致:allowed_* 为空时传 undefined(None=不限),denied_* 空数组即"不禁"
|
// 与专家一致:allowed_* 为空时传 undefined(None=不限),denied_* 空数组即"不禁"
|
||||||
|
// 必须包含全部 6 个字段,否则后端 #[serde(default)] 会让缺失字段变为 None/vec![],
|
||||||
|
// 经 next_capability 完全覆盖原 capability,导致既有策略被清空(数据丢失)
|
||||||
const capability: CapabilityPolicy = {
|
const capability: CapabilityPolicy = {
|
||||||
allowed_skills: editingSubagent.allowedSkills.length > 0 ? editingSubagent.allowedSkills : undefined,
|
allowed_skills: editingSubagent.allowedSkills.length > 0 ? editingSubagent.allowedSkills : undefined,
|
||||||
denied_skills: editingSubagent.deniedSkills,
|
denied_skills: editingSubagent.deniedSkills,
|
||||||
allowed_tools: editingSubagent.allowedTools.length > 0 ? editingSubagent.allowedTools : undefined,
|
allowed_tools: editingSubagent.allowedTools.length > 0 ? editingSubagent.allowedTools : undefined,
|
||||||
denied_tools: editingSubagent.deniedTools,
|
denied_tools: editingSubagent.deniedTools,
|
||||||
|
allowed_subagents: editingSubagent.allowedSubagents.length > 0 ? editingSubagent.allowedSubagents : undefined,
|
||||||
|
denied_subagents: editingSubagent.deniedSubagents,
|
||||||
}
|
}
|
||||||
const resp = await updateSubagentCb({
|
const resp = await updateSubagentCb({
|
||||||
name: editingSubagent.name,
|
name: editingSubagent.name,
|
||||||
@ -1077,13 +1150,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl">
|
<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 p-6 w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] overflow-y-auto">
|
<div
|
||||||
<div className="flex items-center gap-2 mb-4">
|
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"
|
||||||
<Bot className="h-5 w-5 text-[var(--accent-cyan)]" />
|
onClick={e => e.stopPropagation()}
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">编辑子代理</h3>
|
>
|
||||||
</div>
|
<ModalHeader
|
||||||
<div className="space-y-3">
|
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="名称">
|
<Field label="名称">
|
||||||
<input
|
<input
|
||||||
value={editingSubagent.name}
|
value={editingSubagent.name}
|
||||||
@ -1098,7 +1176,9 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2 border-t border-[var(--border-color)]">
|
</SectionCard>
|
||||||
|
<SectionCard title="技能能力" subtitle="白名单取交集,黑名单扣除">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<Field label="允许的技能(白名单)" hint="留空表示不限。仅勾选的 SKILL.md 技能可见">
|
<Field label="允许的技能(白名单)" hint="留空表示不限。仅勾选的 SKILL.md 技能可见">
|
||||||
<CheckboxList
|
<CheckboxList
|
||||||
options={skillOptions}
|
options={skillOptions}
|
||||||
@ -1119,6 +1199,10 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
groupBy={o => o.group ?? '其他'}
|
groupBy={o => o.group ?? '其他'}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
<SectionCard title="工具能力" subtitle="含 mcp_*">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<Field label="允许的工具(白名单)" hint="留空表示不限。覆盖内置 + MCP 工具">
|
<Field label="允许的工具(白名单)" hint="留空表示不限。覆盖内置 + MCP 工具">
|
||||||
<CheckboxList
|
<CheckboxList
|
||||||
options={toolOptions}
|
options={toolOptions}
|
||||||
@ -1140,13 +1224,38 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</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 && (
|
{editingSubagentError && (
|
||||||
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||||
{editingSubagentError}
|
{editingSubagentError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 justify-end mt-5">
|
<ModalFooter>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingSubagent(null)}
|
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"
|
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 ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||||
{savingSubagent ? '保存中...' : '保存'}
|
{savingSubagent ? '保存中...' : '保存'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</ModalFooter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export interface GatewayConfig { host: string; port: number; show_tool_results:
|
|||||||
export interface TimeConfig { timezone: string }
|
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 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 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 ToolsConfig { disabled: string[]; task: TaskConfig }
|
||||||
export interface MemoryMaintenanceConfig { max_merge_ratio: number; min_memories_to_keep: number; max_merge_per_group: number }
|
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 }
|
export interface ImageContextConfig { max_images_in_context: number; max_image_age_rounds: number }
|
||||||
@ -57,6 +57,8 @@ export interface CapabilityPolicy {
|
|||||||
denied_skills: string[]
|
denied_skills: string[]
|
||||||
allowed_tools?: string[]
|
allowed_tools?: string[]
|
||||||
denied_tools: string[]
|
denied_tools: string[]
|
||||||
|
allowed_subagents?: string[]
|
||||||
|
denied_subagents: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubagentItem {
|
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({
|
export function SourceEditor({
|
||||||
sources,
|
sources,
|
||||||
onChange,
|
onChange,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user