feat(web): 专家/子代理 capability 字段改为勾选式编辑
- 专家编辑模态框:4 个 textarea 替换为 CheckboxList,技能/工具按来源分组展示 - 新增子代理编辑模态框:非 builtin 子代理显示编辑按钮,支持描述 + 4 个 capability 勾选字段 - 进入 experts/subagents 标签页时自动拉取技能与工具列表 - allowed_* 为空时传 undefined(后端 None=不限),避免空数组触发白名单空集语义
This commit is contained in:
parent
3ea0c19262
commit
9381ed5dd4
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import {
|
import {
|
||||||
Settings, Save, X, Plus, Trash2, AlertTriangle, Loader2, Wifi,
|
Settings, Save, X, Plus, Trash2, AlertTriangle, Loader2, Wifi,
|
||||||
CheckCircle, RefreshCw, UserCheck, Pencil,
|
CheckCircle, RefreshCw, UserCheck, Pencil, Bot,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
// ── Extracted modules ─────────────────────────────────
|
// ── Extracted modules ─────────────────────────────────
|
||||||
@ -10,16 +10,19 @@ import type {
|
|||||||
ProviderConfig, ModelConfig, AgentConfig,
|
ProviderConfig, ModelConfig, AgentConfig,
|
||||||
McpServerConfig, McpStatusResponse,
|
McpServerConfig, McpStatusResponse,
|
||||||
SkillListResponse,
|
SkillListResponse,
|
||||||
SubagentListResponse,
|
SubagentListResponse, SubagentItem,
|
||||||
|
ToolsListResponse,
|
||||||
ExpertItem, ExpertListResponse,
|
ExpertItem, ExpertListResponse,
|
||||||
|
CapabilityPolicy,
|
||||||
KnownSource,
|
KnownSource,
|
||||||
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 } from './ui'
|
import { Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader, CheckboxList } 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 { listSubagents, toggleSubagent } from '../../api/subagents'
|
import { listTools } from '../../api/tools'
|
||||||
|
import { listSubagents, toggleSubagent, updateSubagent } from '../../api/subagents'
|
||||||
import { listExperts, toggleExpert, createExpert, updateExpert, deleteExpert } from '../../api/experts'
|
import { listExperts, toggleExpert, createExpert, updateExpert, deleteExpert } from '../../api/experts'
|
||||||
import { getMcpStatus } from '../../api/mcp'
|
import { getMcpStatus } from '../../api/mcp'
|
||||||
export { getSelectedExpert, selectExpert } from '../../api/experts'
|
export { getSelectedExpert, selectExpert } from '../../api/experts'
|
||||||
@ -47,6 +50,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
const [mcpStatus, setMcpStatus] = useState<McpStatusResponse | null>(null)
|
const [mcpStatus, setMcpStatus] = useState<McpStatusResponse | null>(null)
|
||||||
const [skillList, setSkillList] = useState<SkillListResponse | null>(null)
|
const [skillList, setSkillList] = useState<SkillListResponse | null>(null)
|
||||||
const [skillListLoading, setSkillListLoading] = useState(false)
|
const [skillListLoading, setSkillListLoading] = useState(false)
|
||||||
|
const [toolList, setToolList] = useState<ToolsListResponse | null>(null)
|
||||||
|
const [toolListLoading, setToolListLoading] = useState(false)
|
||||||
const [subagentList, setSubagentList] = useState<SubagentListResponse | null>(null)
|
const [subagentList, setSubagentList] = useState<SubagentListResponse | null>(null)
|
||||||
const [subagentListLoading, setSubagentListLoading] = useState(false)
|
const [subagentListLoading, setSubagentListLoading] = useState(false)
|
||||||
const [expertList, setExpertList] = useState<ExpertListResponse | null>(null)
|
const [expertList, setExpertList] = useState<ExpertListResponse | null>(null)
|
||||||
@ -58,9 +63,23 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
nameField: string
|
nameField: string
|
||||||
description: string
|
description: string
|
||||||
body: string
|
body: string
|
||||||
|
allowedSkills: string[]
|
||||||
|
deniedSkills: string[]
|
||||||
|
allowedTools: string[]
|
||||||
|
deniedTools: string[]
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [editingExpertError, setEditingExpertError] = useState('')
|
const [editingExpertError, setEditingExpertError] = useState('')
|
||||||
const [savingExpert, setSavingExpert] = useState(false)
|
const [savingExpert, setSavingExpert] = useState(false)
|
||||||
|
const [editingSubagent, setEditingSubagent] = useState<{
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
allowedSkills: string[]
|
||||||
|
deniedSkills: string[]
|
||||||
|
allowedTools: string[]
|
||||||
|
deniedTools: string[]
|
||||||
|
} | null>(null)
|
||||||
|
const [editingSubagentError, setEditingSubagentError] = useState('')
|
||||||
|
const [savingSubagent, setSavingSubagent] = useState(false)
|
||||||
|
|
||||||
const fetchMcpStatus = useCallback(async () => {
|
const fetchMcpStatus = useCallback(async () => {
|
||||||
const data = await getMcpStatus()
|
const data = await getMcpStatus()
|
||||||
@ -74,6 +93,13 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
setSkillListLoading(false)
|
setSkillListLoading(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const fetchToolList = useCallback(async () => {
|
||||||
|
setToolListLoading(true)
|
||||||
|
const data = await listTools()
|
||||||
|
if (data) setToolList(data)
|
||||||
|
setToolListLoading(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const toggleSkillCb = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
const toggleSkillCb = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
||||||
return toggleSkill(name, scope, enabled)
|
return toggleSkill(name, scope, enabled)
|
||||||
}, [])
|
}, [])
|
||||||
@ -89,6 +115,10 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
return toggleSubagent(name, scope, enabled)
|
return toggleSubagent(name, scope, enabled)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const updateSubagentCb = useCallback(async (payload: { name: string; description?: string; body?: string; capability?: CapabilityPolicy }) => {
|
||||||
|
return updateSubagent(payload)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const fetchExpertList = useCallback(async () => {
|
const fetchExpertList = useCallback(async () => {
|
||||||
setExpertListLoading(true)
|
setExpertListLoading(true)
|
||||||
const data = await listExperts()
|
const data = await listExperts()
|
||||||
@ -100,11 +130,11 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
return toggleExpert(name, scope, enabled)
|
return toggleExpert(name, scope, enabled)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const createExpertCb = useCallback(async (payload: { name: string; description: string; body: string; scope: string }) => {
|
const createExpertCb = useCallback(async (payload: { name: string; description: string; body: string; scope: string; capability?: CapabilityPolicy }) => {
|
||||||
return createExpert(payload)
|
return createExpert(payload)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const updateExpertCb = useCallback(async (payload: { name: string; scope: string; description?: string; body?: string }) => {
|
const updateExpertCb = useCallback(async (payload: { name: string; scope: string; description?: string; body?: string; capability?: CapabilityPolicy }) => {
|
||||||
return updateExpert(payload)
|
return updateExpert(payload)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -141,6 +171,14 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
if (activeTab === 'subagents') fetchSubagentList()
|
if (activeTab === 'subagents') fetchSubagentList()
|
||||||
}, [activeTab, fetchSubagentList])
|
}, [activeTab, fetchSubagentList])
|
||||||
|
|
||||||
|
// Fetch skills + tools when experts/subagents tab is selected (for capability CheckboxList)
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 'experts' || activeTab === 'subagents') {
|
||||||
|
if (!skillList) fetchSkillList()
|
||||||
|
if (!toolList) fetchToolList()
|
||||||
|
}
|
||||||
|
}, [activeTab, fetchSkillList, fetchToolList, skillList, toolList])
|
||||||
|
|
||||||
// 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()
|
||||||
@ -568,6 +606,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
{renderDiscoveredSubagents()}
|
{renderDiscoveredSubagents()}
|
||||||
|
{renderSubagentModal()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -636,6 +675,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEditSubagent = (subagent: SubagentItem) => {
|
||||||
|
setEditingSubagentError('')
|
||||||
|
setEditingSubagent({
|
||||||
|
name: subagent.name,
|
||||||
|
description: subagent.description,
|
||||||
|
allowedSkills: subagent.capability?.allowed_skills ?? [],
|
||||||
|
deniedSkills: subagent.capability?.denied_skills ?? [],
|
||||||
|
allowedTools: subagent.capability?.allowed_tools ?? [],
|
||||||
|
deniedTools: subagent.capability?.denied_tools ?? [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard title="已发现子代理" subtitle="即时生效">
|
<SectionCard title="已发现子代理" subtitle="即时生效">
|
||||||
{subagentListLoading && subagents.length === 0 ? (
|
{subagentListLoading && subagents.length === 0 ? (
|
||||||
@ -648,6 +699,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{subagents.map(subagent => {
|
{subagents.map(subagent => {
|
||||||
const isEnabled = subagent.disabled_in_scopes.length === 0
|
const isEnabled = subagent.disabled_in_scopes.length === 0
|
||||||
|
const isBuiltin = subagent.source === 'builtin'
|
||||||
return (
|
return (
|
||||||
<div key={subagent.name} className="flex items-center gap-3 py-2 px-2 rounded-lg hover:bg-[var(--bg-hover)] transition-colors">
|
<div key={subagent.name} className="flex items-center gap-3 py-2 px-2 rounded-lg hover:bg-[var(--bg-hover)] transition-colors">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@ -656,9 +708,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-muted)] uppercase tracking-wider">{subagent.source}</span>
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-muted)] uppercase tracking-wider">{subagent.source}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[var(--text-muted)] truncate mt-0.5">{subagent.description}</p>
|
<p className="text-xs text-[var(--text-muted)] truncate mt-0.5">{subagent.description}</p>
|
||||||
{renderToolTags('允许', subagent.allowed_tools, 'allow')}
|
{renderToolTags('允许', subagent.capability?.allowed_tools, 'allow')}
|
||||||
{renderToolTags('禁用', subagent.denied_tools, 'deny')}
|
{renderToolTags('禁用', subagent.capability?.denied_tools, 'deny')}
|
||||||
</div>
|
</div>
|
||||||
|
{!isBuiltin && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleEditSubagent(subagent)}
|
||||||
|
className="p-1 rounded text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||||
|
title="编辑"
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<Toggle checked={isEnabled} onChange={() => handleToggle(subagent.name, isEnabled)} />
|
<Toggle checked={isEnabled} onChange={() => handleToggle(subagent.name, isEnabled)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -691,7 +752,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingExpertError('')
|
setEditingExpertError('')
|
||||||
setEditingExpert({ mode: 'create', scope: 'project', nameField: '', description: '', body: '' })
|
setEditingExpert({ mode: 'create', scope: 'project', nameField: '', description: '', body: '', allowedSkills: [], deniedSkills: [], allowedTools: [], deniedTools: [] })
|
||||||
}}
|
}}
|
||||||
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"
|
||||||
>
|
>
|
||||||
@ -750,6 +811,10 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
nameField: expert.name,
|
nameField: expert.name,
|
||||||
description: expert.description,
|
description: expert.description,
|
||||||
body: expert.body ?? '',
|
body: expert.body ?? '',
|
||||||
|
allowedSkills: expert.capability?.allowed_skills ?? [],
|
||||||
|
deniedSkills: expert.capability?.denied_skills ?? [],
|
||||||
|
allowedTools: expert.capability?.allowed_tools ?? [],
|
||||||
|
deniedTools: expert.capability?.denied_tools ?? [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -817,6 +882,12 @@ 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 skillEmptyHint = skillListLoading ? '加载中...' : '未发现任何技能,请先在技能页配置来源目录'
|
||||||
|
const toolEmptyHint = toolListLoading ? '加载中...' : '未发现任何工具'
|
||||||
|
|
||||||
const renderExpertModal = () => {
|
const renderExpertModal = () => {
|
||||||
if (!editingExpert) return null
|
if (!editingExpert) return null
|
||||||
const isEdit = editingExpert.mode === 'edit'
|
const isEdit = editingExpert.mode === 'edit'
|
||||||
@ -827,19 +898,24 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
setSavingExpert(true)
|
setSavingExpert(true)
|
||||||
setEditingExpertError('')
|
setEditingExpertError('')
|
||||||
try {
|
try {
|
||||||
|
// allowed_skills/allowed_tools 为空时必须传 undefined(后端 None=不限),
|
||||||
|
// 否则空数组会被反序列化为 Some(vec![]) 触发白名单空集语义(全禁)。
|
||||||
|
// denied_skills/denied_tools 为 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,
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
name: editingExpert.nameField,
|
||||||
|
description: editingExpert.description,
|
||||||
|
body: editingExpert.body,
|
||||||
|
capability,
|
||||||
|
}
|
||||||
const resp = isEdit
|
const resp = isEdit
|
||||||
? await updateExpertCb({
|
? await updateExpertCb({ ...payload, scope: 'project' })
|
||||||
name: editingExpert.nameField,
|
: await createExpertCb({ ...payload, scope: 'project' })
|
||||||
scope: 'project',
|
|
||||||
description: editingExpert.description,
|
|
||||||
body: editingExpert.body,
|
|
||||||
})
|
|
||||||
: await createExpertCb({
|
|
||||||
name: editingExpert.nameField,
|
|
||||||
description: editingExpert.description,
|
|
||||||
body: editingExpert.body,
|
|
||||||
scope: 'project',
|
|
||||||
})
|
|
||||||
const data = await resp.json().catch(() => ({}))
|
const data = await resp.json().catch(() => ({}))
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
setEditingExpertError(data.error || data.message || '保存失败')
|
setEditingExpertError(data.error || data.message || '保存失败')
|
||||||
@ -859,7 +935,7 @@ 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">
|
||||||
<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]">
|
<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">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<UserCheck className="h-5 w-5 text-[var(--accent-cyan)]" />
|
<UserCheck className="h-5 w-5 text-[var(--accent-cyan)]" />
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">
|
<h3 className="text-sm font-semibold text-[var(--text-primary)]">
|
||||||
@ -890,9 +966,51 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
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-[360px] resize-y font-mono text-xs'}
|
className={inputCls + ' min-h-[200px] 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)]">
|
||||||
|
<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 ?? '其他'}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
{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}
|
||||||
@ -920,6 +1038,135 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderSubagentModal = () => {
|
||||||
|
if (!editingSubagent) return null
|
||||||
|
const canSave = editingSubagent.description.trim().length > 0
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!canSave) return
|
||||||
|
setSavingSubagent(true)
|
||||||
|
setEditingSubagentError('')
|
||||||
|
try {
|
||||||
|
// 与专家一致:allowed_* 为空时传 undefined(None=不限),denied_* 空数组即"不禁"
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
const resp = await updateSubagentCb({
|
||||||
|
name: editingSubagent.name,
|
||||||
|
description: editingSubagent.description,
|
||||||
|
capability,
|
||||||
|
})
|
||||||
|
const data = await resp.json().catch(() => ({}))
|
||||||
|
if (!resp.ok) {
|
||||||
|
setEditingSubagentError(data.error || data.message || '保存失败')
|
||||||
|
setSavingSubagent(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setToast('子代理已更新')
|
||||||
|
setTimeout(() => setToast(''), 3000)
|
||||||
|
setEditingSubagent(null)
|
||||||
|
fetchSubagentList()
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setEditingSubagentError(e instanceof Error ? e.message : '网络错误')
|
||||||
|
} finally {
|
||||||
|
setSavingSubagent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ?? '其他'}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
{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">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!canSave || savingSubagent}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white bg-[var(--accent-cyan)]/20 border border-[var(--accent-cyan)]/30 hover:bg-[var(--accent-cyan)]/30 hover:border-[var(--accent-cyan)]/50 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{savingSubagent ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||||
|
{savingSubagent ? '保存中...' : '保存'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const renderMcp = () => {
|
const renderMcp = () => {
|
||||||
const entries = Object.entries(config.mcpServers)
|
const entries = Object.entries(config.mcpServers)
|
||||||
const statusFor = (key: string) => mcpStatus?.servers?.find(s => s.key === key)
|
const statusFor = (key: string) => mcpStatus?.servers?.find(s => s.key === key)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user