优化tool选择的下拉框:根据插件分组

This commit is contained in:
advent259141
2026-01-27 00:21:57 +08:00
parent 1bd8eae25a
commit 053c4e989b
+156 -16
View File
@@ -120,23 +120,45 @@
</v-col>
<v-col cols="12">
<v-autocomplete
v-model="agent.tools"
:items="toolOptions"
v-model="agent.__tool_group"
:items="toolGroupOptions"
item-title="title"
item-value="value"
label="分配工具(多选)"
label="选择插件/来源"
variant="outlined"
density="comfortable"
class="subagent-tools"
:loading="toolsLoading"
:disabled="toolsLoading"
clearable
@update:modelValue="onGroupChanged(agent)"
/>
</v-col>
<v-col cols="12">
<v-autocomplete
v-model="agent.__tool_group_selected"
:items="getToolOptionsByGroup(agent.__tool_group)"
item-title="title"
item-value="value"
label="选择该插件下的工具(多选)"
variant="outlined"
density="comfortable"
class="subagent-tools"
multiple
chips
closable-chips
:menu-props="{ maxHeight: 320 }"
:menu-props="{ maxHeight: 380 }"
:max-chips="8"
:loading="toolsLoading"
:disabled="toolsLoading"
:disabled="toolsLoading || !agent.__tool_group"
clearable
@update:modelValue="syncGroupSelectionToAgentTools(agent)"
/>
<div class="text-caption text-medium-emphasis mt-1">
已分配{{ (agent.tools || []).length }} 个工具
</div>
</v-col>
</v-row>
@@ -198,6 +220,12 @@ import ProviderSelector from '@/components/shared/ProviderSelector.vue'
type ToolOption = { title: string; value: string }
type ToolGroup = {
key: string
label: string
options: ToolOption[]
}
type SubAgentItem = {
__key: string
name: string
@@ -206,6 +234,9 @@ type SubAgentItem = {
tools: string[]
enabled: boolean
provider_id?: string
// UI-only: current tool group selection state
__tool_group?: string
__tool_group_selected?: string[]
}
type SubAgentConfig = {
@@ -240,7 +271,58 @@ const cfg = ref<SubAgentConfig>({
})
const toolOptions = ref<ToolOption[]>([])
const toolGroups = ref<ToolGroup[]>([])
const toolGroupOptions = ref<{ title: string; value: string }[]>([])
function modulePathToLabel(mp: unknown): string {
const raw = (mp ?? '').toString().trim()
if (!raw) return '其他/未归类'
// Typical module paths look like:
// - data.plugins.<plugin_name>.main
// - astrbot.builtin_stars.<star_name>.main
// - astrbot.plugins.<plugin_name>.main
// We strip common prefixes and the trailing ".main" for display.
const trimmed = raw.replace(/\.main$/, '')
if (trimmed.startsWith('data.plugins.')) return trimmed.replace(/^data\.plugins\./, '')
if (trimmed.startsWith('astrbot.builtin_stars.')) return `builtin: ${trimmed.replace(/^astrbot\.builtin_stars\./, '')}`
if (trimmed.startsWith('astrbot.plugins.')) return trimmed.replace(/^astrbot\.plugins\./, '')
if (raw.startsWith('plugins.')) return raw.replace(/^plugins\./, '')
if (raw.startsWith('builtin_stars.')) return `builtin: ${raw.replace(/^builtin_stars\./, '')}`
if (raw.startsWith('core.')) return `core: ${raw.replace(/^core\./, '')}`
return raw
}
function rebuildToolGroupOptions() {
toolGroupOptions.value = toolGroups.value.map(g => ({ title: g.label, value: g.key }))
}
function getToolOptionsByGroup(groupKey: string | undefined): ToolOption[] {
if (!groupKey) return []
return toolGroups.value.find(g => g.key === groupKey)?.options ?? []
}
function onGroupChanged(agent: SubAgentItem) {
// When switching groups, reflect already-assigned tools for that group.
const groupOptions = getToolOptionsByGroup(agent.__tool_group)
const allowed = new Set(groupOptions.map(o => o.value))
agent.__tool_group_selected = (agent.tools || []).filter(t => allowed.has(t))
}
function syncGroupSelectionToAgentTools(agent: SubAgentItem) {
const groupOptions = getToolOptionsByGroup(agent.__tool_group)
const allowed = new Set(groupOptions.map(o => o.value))
const selected = Array.isArray(agent.__tool_group_selected)
? agent.__tool_group_selected
: []
// Replace only tools belonging to this group; keep tools from other groups intact.
const kept = (agent.tools || []).filter(t => !allowed.has(t))
const merged = [...kept, ...selected.filter(t => allowed.has(t))]
const seen = new Set<string>()
agent.tools = merged.filter(t => (seen.has(t) ? false : (seen.add(t), true)))
}
function normalizeConfig(raw: any): SubAgentConfig {
const main_enable = !!raw?.main_enable
@@ -263,7 +345,9 @@ function normalizeConfig(raw: any): SubAgentConfig {
tools,
enabled
,
provider_id
provider_id,
__tool_group: undefined,
__tool_group_selected: []
}
})
@@ -293,13 +377,27 @@ async function loadTools() {
const res = await axios.get('/api/subagent/available-tools')
if (res.data.status === 'ok') {
const list = Array.isArray(res.data.data) ? res.data.data : []
toolOptions.value = list
.filter((t: any) => !!t?.name)
.map((t: any) => {
const name = String(t.name)
const desc = (t.description ?? '').toString().trim()
return { title: desc ? `${name}${desc}` : name, value: name }
})
const groups = new Map<string, ToolOption[]>()
for (const t of list) {
if (!t?.name) continue
const name = String(t.name)
const desc = (t.description ?? '').toString().trim()
const mp = (t.handler_module_path ?? '').toString()
const key = mp || '__other__'
const options = groups.get(key) ?? []
options.push({ title: desc ? `${name}${desc}` : name, value: name })
groups.set(key, options)
}
toolGroups.value = Array.from(groups.entries())
.map(([key, options]) => ({
key,
label: modulePathToLabel(key === '__other__' ? '' : key),
options: options.sort((a, b) => a.value.localeCompare(b.value))
}))
.sort((a, b) => a.label.localeCompare(b.label))
rebuildToolGroupOptions()
} else {
toast(res.data.message || '获取工具列表失败', 'error')
}
@@ -309,13 +407,23 @@ async function loadTools() {
const res2 = await axios.get('/api/tools/list')
if (res2.data.status === 'ok') {
const list = Array.isArray(res2.data.data) ? res2.data.data : []
toolOptions.value = list
const options = list
.filter((t: any) => !!t?.name)
.map((t: any) => {
const name = String(t.name)
const desc = (t.description ?? '').toString().trim()
return { title: desc ? `${name}${desc}` : name, value: name }
})
.sort((a: ToolOption, b: ToolOption) => a.value.localeCompare(b.value))
toolGroups.value = [
{
key: '__all__',
label: '全部工具',
options
}
]
rebuildToolGroupOptions()
}
} catch {
toast('获取工具列表失败', 'error')
@@ -333,7 +441,9 @@ function addAgent() {
system_prompt: '',
tools: [],
enabled: true,
provider_id: undefined
provider_id: undefined,
__tool_group: undefined,
__tool_group_selected: []
})
}
@@ -341,7 +451,30 @@ function removeAgent(idx: number) {
cfg.value.agents.splice(idx, 1)
}
function validateBeforeSave(): boolean {
const nameRe = /^[a-z][a-z0-9_]{0,63}$/
const seen = new Set<string>()
for (const a of cfg.value.agents) {
const name = (a.name || '').trim()
if (!name) {
toast('存在未填写名称的 SubAgent', 'warning')
return false
}
if (!nameRe.test(name)) {
toast('SubAgent 名称不合法:仅允许英文小写字母/数字/下划线,且需以字母开头', 'warning')
return false
}
if (seen.has(name)) {
toast(`SubAgent 名称重复:${name}`, 'warning')
return false
}
seen.add(name)
}
return true
}
async function save() {
if (!validateBeforeSave()) return
saving.value = true
try {
// Strip UI-only fields
@@ -374,6 +507,12 @@ async function save() {
async function reload() {
await Promise.all([loadConfig(), loadTools()])
// Initialize UI-only selections after tools load.
for (const a of cfg.value.agents) {
if (!a.__tool_group) a.__tool_group = undefined
if (!Array.isArray(a.__tool_group_selected)) a.__tool_group_selected = []
}
}
onMounted(() => {
@@ -429,6 +568,7 @@ onMounted(() => {
max-width: 520px;
}
.subagent-title-right {
display: flex;
align-items: center;