fix: all mcp tools exposed to main agent (#5252)
This commit is contained in:
@@ -42,7 +42,6 @@ from astrbot.core.message.components import File, Image, Reply
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.provider import Provider
|
||||
from astrbot.core.provider.entities import ProviderRequest
|
||||
from astrbot.core.provider.manager import llm_tools
|
||||
from astrbot.core.skills.skill_manager import SkillManager, build_skills_prompt
|
||||
from astrbot.core.star.context import Context
|
||||
from astrbot.core.star.star_handler import star_map
|
||||
@@ -770,14 +769,6 @@ def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None:
|
||||
if plugin.name in event.plugins_name or plugin.reserved:
|
||||
new_tool_set.add_tool(tool)
|
||||
req.func_tool = new_tool_set
|
||||
else:
|
||||
# mcp tools
|
||||
tool_set = req.func_tool
|
||||
if not tool_set:
|
||||
tool_set = ToolSet()
|
||||
for tool in llm_tools.func_list:
|
||||
if isinstance(tool, MCPTool):
|
||||
tool_set.add_tool(tool)
|
||||
|
||||
|
||||
async def _handle_webchat(
|
||||
|
||||
@@ -4,6 +4,7 @@ import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { ref, computed } from 'vue'
|
||||
import ConfigItemRenderer from './ConfigItemRenderer.vue'
|
||||
import TemplateListEditor from './TemplateListEditor.vue'
|
||||
import PersonaQuickPreview from './PersonaQuickPreview.vue'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
|
||||
@@ -274,6 +275,16 @@ function getSpecialSubtype(value) {
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Default Persona Quick Preview 全宽显示区域 -->
|
||||
<v-row
|
||||
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_persona' && itemKey === 'provider_settings.default_personality'"
|
||||
class="persona-preview-row"
|
||||
>
|
||||
<v-col cols="12" class="persona-preview-display">
|
||||
<PersonaQuickPreview :model-value="createSelectorModel(itemKey).value" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-divider class="config-divider"
|
||||
v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"></v-divider>
|
||||
@@ -433,6 +444,15 @@ function getSpecialSubtype(value) {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.persona-preview-row {
|
||||
margin: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.persona-preview-display {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.selected-plugins-full-width {
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.1);
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="persona-preview-card">
|
||||
<div class="preview-header">
|
||||
<small>{{ tm('personaQuickPreview.title') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="preview-loading">
|
||||
<v-progress-circular indeterminate size="18" width="2" color="primary" class="mr-2" />
|
||||
<small class="text-grey">{{ tm('personaQuickPreview.loading') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!modelValue" class="preview-empty">
|
||||
<small class="text-grey">{{ tm('personaQuickPreview.noPersonaSelected') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!personaData" class="preview-empty">
|
||||
<small class="text-grey">{{ tm('personaQuickPreview.personaNotFound') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-else class="preview-content">
|
||||
<div class="section-title">{{ tm('personaQuickPreview.systemPromptLabel') }}</div>
|
||||
<pre class="prompt-content">{{ personaData.system_prompt || '' }}</pre>
|
||||
|
||||
<div class="section-title mt-3">{{ tm('personaQuickPreview.toolsLabel') }}</div>
|
||||
<div class="chip-wrap tools-wrap">
|
||||
<v-chip
|
||||
v-if="personaData.tools === null"
|
||||
size="small"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
label
|
||||
>
|
||||
{{ tm('personaQuickPreview.allToolsWithCount', { count: allToolsCount }) }}
|
||||
</v-chip>
|
||||
<div v-for="tool in resolvedTools" v-else :key="tool.name" class="tool-item">
|
||||
<v-chip
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
label
|
||||
>
|
||||
{{ tool.name }}
|
||||
</v-chip>
|
||||
<small v-if="tool.origin || tool.origin_name" class="text-grey tool-meta">
|
||||
<span v-if="tool.origin">{{ tm('personaQuickPreview.originLabel') }}: {{ tool.origin }}</span>
|
||||
<span v-if="tool.origin_name"> | {{ tm('personaQuickPreview.originNameLabel') }}: {{ tool.origin_name }}</span>
|
||||
</small>
|
||||
</div>
|
||||
<small v-if="personaData.tools !== null && normalizedTools.length === 0" class="text-grey">
|
||||
{{ tm('personaQuickPreview.noTools') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="section-title mt-3">{{ tm('personaQuickPreview.skillsLabel') }}</div>
|
||||
<div class="chip-wrap">
|
||||
<v-chip
|
||||
v-if="personaData.skills === null"
|
||||
size="small"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
label
|
||||
>
|
||||
{{ tm('personaQuickPreview.allSkillsWithCount', { count: allSkillsCount }) }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-for="skillName in normalizedSkills"
|
||||
v-else
|
||||
:key="skillName"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
label
|
||||
>
|
||||
{{ skillName }}
|
||||
</v-chip>
|
||||
<small v-if="personaData.skills !== null && normalizedSkills.length === 0" class="text-grey">
|
||||
{{ tm('personaQuickPreview.noSkills') }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const { tm } = useModuleI18n('core.shared')
|
||||
|
||||
const loading = ref(false)
|
||||
const personaData = ref(null)
|
||||
const toolMetaMap = ref({})
|
||||
const availableSkills = ref([])
|
||||
|
||||
const defaultPersonaData = {
|
||||
persona_id: 'default',
|
||||
system_prompt: 'You are a helpful and friendly assistant.',
|
||||
tools: null,
|
||||
skills: null
|
||||
}
|
||||
|
||||
const normalizedTools = computed(() => (Array.isArray(personaData.value?.tools) ? personaData.value.tools : []))
|
||||
const normalizedSkills = computed(() => (Array.isArray(personaData.value?.skills) ? personaData.value.skills : []))
|
||||
const allToolsCount = computed(() => Object.keys(toolMetaMap.value).length)
|
||||
const allSkillsCount = computed(() => availableSkills.value.length)
|
||||
const resolvedTools = computed(() =>
|
||||
normalizedTools.value.map((toolName) => {
|
||||
const meta = toolMetaMap.value[toolName] || {}
|
||||
return {
|
||||
name: toolName,
|
||||
origin: meta.origin || '',
|
||||
origin_name: meta.origin_name || ''
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
async function loadToolsMeta() {
|
||||
try {
|
||||
const response = await axios.get('/api/tools/list')
|
||||
if (response.data?.status === 'ok') {
|
||||
const tools = response.data?.data || []
|
||||
const nextMap = {}
|
||||
for (const tool of tools) {
|
||||
if (!tool?.name) {
|
||||
continue
|
||||
}
|
||||
nextMap[tool.name] = {
|
||||
origin: tool.origin || '',
|
||||
origin_name: tool.origin_name || ''
|
||||
}
|
||||
}
|
||||
toolMetaMap.value = nextMap
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load tools metadata:', error)
|
||||
toolMetaMap.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkillsMeta() {
|
||||
try {
|
||||
const response = await axios.get('/api/skills')
|
||||
if (response.data?.status === 'ok') {
|
||||
const payload = response.data?.data || []
|
||||
if (Array.isArray(payload)) {
|
||||
availableSkills.value = payload.filter((skill) => skill.active !== false)
|
||||
} else {
|
||||
const skills = payload.skills || []
|
||||
availableSkills.value = skills.filter((skill) => skill.active !== false)
|
||||
}
|
||||
} else {
|
||||
availableSkills.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load skills metadata:', error)
|
||||
availableSkills.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPersonaPreview(personaId) {
|
||||
if (!personaId) {
|
||||
personaData.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (personaId === 'default') {
|
||||
personaData.value = defaultPersonaData
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/persona/list')
|
||||
if (response.data?.status === 'ok') {
|
||||
const personas = response.data?.data || []
|
||||
personaData.value = personas.find((item) => item.persona_id === personaId) || null
|
||||
} else {
|
||||
personaData.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load persona preview:', error)
|
||||
personaData.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePersonaSaved() {
|
||||
if (props.modelValue) {
|
||||
loadPersonaPreview(props.modelValue)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
loadPersonaPreview(newValue)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
loadToolsMeta()
|
||||
loadSkillsMeta()
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('astrbot:persona-saved', handlePersonaSaved)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('astrbot:persona-saved', handlePersonaSaved)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.persona-preview-card {
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preview-loading,
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.75rem;
|
||||
color: rgb(var(--v-theme-primaryText));
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.prompt-content {
|
||||
margin-top: 6px;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.chip-wrap {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.tools-wrap {
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tool-meta {
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.tools-wrap {
|
||||
max-height: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -188,10 +188,16 @@ function openEditPersona(persona: Persona) {
|
||||
// 人格保存成功(创建或编辑)
|
||||
async function handlePersonaSaved(message: string) {
|
||||
console.log('人格保存成功:', message)
|
||||
const savedPersonaId = editingPersona.value?.persona_id || ''
|
||||
showPersonaDialog.value = false
|
||||
editingPersona.value = null
|
||||
// 刷新当前文件夹的人格列表
|
||||
await loadPersonasInFolder(currentFolderId.value)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('astrbot:persona-saved', {
|
||||
detail: { persona_id: savedPersonaId }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
|
||||
@@ -62,6 +62,23 @@
|
||||
"rootFolder": "All Personas",
|
||||
"emptyFolder": "This folder is empty"
|
||||
},
|
||||
"personaQuickPreview": {
|
||||
"title": "Quick Persona Preview",
|
||||
"loading": "Loading...",
|
||||
"noPersonaSelected": "No persona selected",
|
||||
"personaNotFound": "Persona details not found",
|
||||
"systemPromptLabel": "System Prompt",
|
||||
"toolsLabel": "Tools",
|
||||
"skillsLabel": "Skills",
|
||||
"originLabel": "Origin",
|
||||
"originNameLabel": "Origin Name",
|
||||
"allTools": "All tools available",
|
||||
"allToolsWithCount": "All tools available ({count})",
|
||||
"noTools": "No tools configured",
|
||||
"allSkills": "All Skills available",
|
||||
"allSkillsWithCount": "All Skills available ({count})",
|
||||
"noSkills": "No Skills configured"
|
||||
},
|
||||
"t2iTemplateEditor": {
|
||||
"buttonText": "Customize T2I Template",
|
||||
"dialogTitle": "Customize Text-to-Image HTML Template",
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"enabled": "When on: the main LLM keeps its own tools and mounts transfer_to_* delegate tools. With deduplication, tools overlapping with SubAgents are removed from the main tool set."
|
||||
},
|
||||
"section": {
|
||||
"title": "SubAgents"
|
||||
"title": "SubAgents",
|
||||
"globalSettings": "Global Settings"
|
||||
},
|
||||
"cards": {
|
||||
"statusEnabled": "Enabled",
|
||||
|
||||
@@ -62,6 +62,23 @@
|
||||
"rootFolder": "全部人格",
|
||||
"emptyFolder": "此文件夹为空"
|
||||
},
|
||||
"personaQuickPreview": {
|
||||
"title": "快速预览",
|
||||
"loading": "加载中...",
|
||||
"noPersonaSelected": "未选择人格",
|
||||
"personaNotFound": "未找到该人格的详情",
|
||||
"systemPromptLabel": "系统提示词",
|
||||
"toolsLabel": "工具",
|
||||
"skillsLabel": "技能(Skills)",
|
||||
"originLabel": "来源",
|
||||
"originNameLabel": "来源名称",
|
||||
"allTools": "全部工具可用",
|
||||
"allToolsWithCount": "全部工具可用({count})",
|
||||
"noTools": "未配置工具",
|
||||
"allSkills": "全部 Skills 可用",
|
||||
"allSkillsWithCount": "全部 Skills 可用({count})",
|
||||
"noSkills": "未配置 Skills"
|
||||
},
|
||||
"t2iTemplateEditor": {
|
||||
"buttonText": "自定义 T2I 模板",
|
||||
"dialogTitle": "自定义文转图 HTML 模板",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"presetDialogsHelp": "添加一些预设的对话来帮助机器人更好地理解角色设定。",
|
||||
"userMessage": "用户消息",
|
||||
"assistantMessage": "AI 回答",
|
||||
"tools": "工具选择",
|
||||
"tools": "工具 / MCP 工具选择",
|
||||
"toolsHelp": "为这个人格选择可用的外部工具。外部工具给了 AI 接触外部环境的能力,如搜索、计算、获取信息等。",
|
||||
"toolsSelection": "工具选择操作",
|
||||
"selectAllTools": "选择所有工具",
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"enabled": "启动:主 LLM 会保留自身工具并挂载 transfer_to_* 委派工具。若开启“去重重复工具”,与 SubAgent 指定的工具重叠部分会从主 LLM 工具集中移除。"
|
||||
},
|
||||
"section": {
|
||||
"title": "SubAgents"
|
||||
"title": "SubAgents 配置",
|
||||
"globalSettings": "全局设置"
|
||||
},
|
||||
"cards": {
|
||||
"statusEnabled": "启用",
|
||||
@@ -28,7 +29,8 @@
|
||||
"transferPrefix": "transfer_to_{name}",
|
||||
"switchLabel": "启用",
|
||||
"previewTitle": "预览:主 LLM 将看到的 handoff 工具",
|
||||
"personaChip": "Persona: {id}"
|
||||
"personaChip": "Persona: {id}",
|
||||
"noDescription": "暂无描述"
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Agent 名称(用于 transfer_to_{name})",
|
||||
|
||||
@@ -1121,7 +1121,7 @@ export default {
|
||||
|
||||
.text-truncate {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
/* max-width: 100px; */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,155 +1,249 @@
|
||||
<template>
|
||||
<div class="subagent-page">
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<div class="d-flex align-center justify-space-between mb-6">
|
||||
<div>
|
||||
<div class="d-flex align-center" style="gap: 8px;">
|
||||
<div class="d-flex align-center gap-2 mb-1">
|
||||
<h2 class="text-h5 font-weight-bold">{{ tm('page.title') }}</h2>
|
||||
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>{{ tm('page.beta') }}</v-chip>
|
||||
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label class="font-weight-bold">
|
||||
{{ tm('page.beta') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('page.subtitle') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center" style="gap: 8px;">
|
||||
<v-btn variant="tonal" color="primary" :loading="loading" @click="reload">{{ tm('actions.refresh') }}</v-btn>
|
||||
<v-btn variant="flat" color="primary" :loading="saving" @click="save">{{ tm('actions.save') }}</v-btn>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="loading"
|
||||
@click="reload"
|
||||
>
|
||||
{{ tm('actions.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="primary"
|
||||
prepend-icon="mdi-content-save"
|
||||
:loading="saving"
|
||||
@click="save"
|
||||
>
|
||||
{{ tm('actions.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-card class="rounded-lg" variant="flat">
|
||||
<!-- Global Settings Card -->
|
||||
<v-card class="rounded-lg mb-6 border-thin" variant="flat" border>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-bold mb-1">{{ tm('section.globalSettings') || 'Global Settings' }}</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ mainStateDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch
|
||||
v-model="cfg.main_enable"
|
||||
:label="tm('switches.enable')"
|
||||
inset
|
||||
color="primary"
|
||||
hide-details
|
||||
inset
|
||||
density="comfortable"
|
||||
/>
|
||||
>
|
||||
<template #label>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-body-2 font-weight-medium">{{ tm('switches.enable') }}</span>
|
||||
<span class="text-caption text-medium-emphasis">Enable sub-agent functionality</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch
|
||||
v-model="cfg.remove_main_duplicate_tools"
|
||||
:disabled="!cfg.main_enable"
|
||||
:label="tm('switches.dedupe')"
|
||||
inset
|
||||
color="primary"
|
||||
hide-details
|
||||
inset
|
||||
density="comfortable"
|
||||
/>
|
||||
>
|
||||
<template #label>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-body-2 font-weight-medium">{{ tm('switches.dedupe') }}</span>
|
||||
<span class="text-caption text-medium-emphasis">Remove duplicate tools from main agent</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="text-caption text-medium-emphasis mt-1">
|
||||
{{ mainStateDescription }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center justify-space-between mt-6 mb-2">
|
||||
<div class="text-subtitle-1 font-weight-bold">{{ tm('section.title') }}</div>
|
||||
<v-btn size="small" variant="tonal" color="primary" @click="addAgent">
|
||||
{{ tm('actions.add') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-expansion-panels variant="accordion" multiple>
|
||||
<v-expansion-panel v-for="(agent, idx) in cfg.agents" :key="agent.__key">
|
||||
<v-expansion-panel-title>
|
||||
<div class="subagent-panel-title">
|
||||
<div class="subagent-title-left">
|
||||
<v-chip :color="agent.enabled ? 'success' : 'grey'" size="small" variant="tonal">
|
||||
{{ agent.enabled ? tm('cards.statusEnabled') : tm('cards.statusDisabled') }}
|
||||
</v-chip>
|
||||
|
||||
<div class="subagent-title-text">
|
||||
<div class="subagent-title-name">{{ agent.name || tm('cards.unnamed') }}</div>
|
||||
<div class="subagent-title-sub">
|
||||
{{ tm('cards.transferPrefix', { name: agent.name || '...' }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subagent-title-right">
|
||||
<v-switch
|
||||
v-model="agent.enabled"
|
||||
inset
|
||||
color="primary"
|
||||
hide-details
|
||||
class="subagent-enabled-inline"
|
||||
@click.stop
|
||||
>
|
||||
<template #label>{{ tm('cards.switchLabel') }}</template>
|
||||
</v-switch>
|
||||
|
||||
<v-btn size="small" variant="text" color="error" @click.stop="removeAgent(idx)">
|
||||
{{ tm('actions.delete') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<v-row class="subagent-grid">
|
||||
<v-col cols="12" md="5">
|
||||
<v-text-field
|
||||
v-model="agent.name"
|
||||
:label="tm('form.nameLabel')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:hint="tm('form.nameHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="7" class="subagent-actions">
|
||||
<ProviderSelector
|
||||
v-model="agent.provider_id"
|
||||
provider-type="chat_completion"
|
||||
:label="tm('form.providerLabel')"
|
||||
:hint="tm('form.providerHint')"
|
||||
persistent-hint
|
||||
clearable
|
||||
class="subagent-provider"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-autocomplete
|
||||
v-model="agent.persona_id"
|
||||
:items="personaOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
:label="tm('form.personaLabel')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
clearable
|
||||
:loading="personaLoading"
|
||||
:disabled="personaLoading"
|
||||
:hint="tm('form.personaHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="agent.public_description"
|
||||
:label="tm('form.descriptionLabel')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:hint="tm('form.descriptionHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3000">
|
||||
<!-- Agents List Section -->
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-icon icon="mdi-robot" color="primary" size="small" />
|
||||
<div class="text-h6 font-weight-bold">{{ tm('section.title') }}</div>
|
||||
<v-chip size="small" variant="tonal" color="primary" class="ml-2">
|
||||
{{ cfg.agents.length }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-btn
|
||||
prepend-icon="mdi-plus"
|
||||
color="primary"
|
||||
@click="addAgent"
|
||||
>
|
||||
{{ tm('actions.add') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-expansion-panels variant="popout" class="subagent-panels">
|
||||
<v-expansion-panel
|
||||
v-for="(agent, idx) in cfg.agents"
|
||||
:key="agent.__key"
|
||||
elevation="0"
|
||||
class="border-thin mb-2 rounded-lg"
|
||||
:class="{ 'border-primary': agent.enabled }"
|
||||
>
|
||||
<v-expansion-panel-title class="py-3">
|
||||
<div class="d-flex align-center w-100 gap-4">
|
||||
<!-- Status Indicator -->
|
||||
<v-badge
|
||||
dot
|
||||
:color="agent.enabled ? 'success' : 'grey'"
|
||||
inline
|
||||
class="mr-2"
|
||||
/>
|
||||
|
||||
<!-- Agent Info -->
|
||||
<div class="d-flex flex-column flex-grow-1" style="min-width: 0;">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<span class="text-subtitle-1 font-weight-bold text-truncate">
|
||||
{{ agent.name || tm('cards.unnamed') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis text-truncate">
|
||||
{{ agent.public_description || tm('cards.noDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls (stop propagation on clicks) -->
|
||||
<div class="d-flex align-center gap-2" @click.stop>
|
||||
<v-switch
|
||||
v-model="agent.enabled"
|
||||
color="success"
|
||||
hide-details
|
||||
inset
|
||||
density="compact"
|
||||
class="mr-2"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete-outline"
|
||||
variant="text"
|
||||
color="error"
|
||||
density="comfortable"
|
||||
@click="removeAgent(idx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<v-divider class="mb-4" />
|
||||
<v-row>
|
||||
<!-- Left Column: Form -->
|
||||
<v-col cols="12" md="6">
|
||||
<div class="d-flex flex-column gap-4">
|
||||
<v-text-field
|
||||
v-model="agent.name"
|
||||
:label="tm('form.nameLabel')"
|
||||
:rules="[v => !!v || 'Name is required', v => /^[a-z][a-z0-9_]*$/.test(v) || 'Lowercase letters, numbers, underscore only']"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div class="text-caption text-medium-emphasis ml-1">{{ tm('form.providerLabel') }}</div>
|
||||
<v-card variant="outlined" class="pa-0 border-thin rounded bg-transparent" style="border-color: rgba(var(--v-border-color), var(--v-border-opacity));">
|
||||
<div class="pa-3">
|
||||
<ProviderSelector
|
||||
v-model="agent.provider_id"
|
||||
provider-type="chat_completion"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div class="text-caption text-medium-emphasis ml-1">{{ tm('form.personaLabel') }}</div>
|
||||
<v-card variant="outlined" class="pa-0 border-thin rounded bg-transparent" style="border-color: rgba(var(--v-border-color), var(--v-border-opacity));">
|
||||
<div class="pa-3">
|
||||
<PersonaSelector
|
||||
v-model="agent.persona_id"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<v-textarea
|
||||
v-model="agent.public_description"
|
||||
:label="tm('form.descriptionLabel')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
auto-grow
|
||||
hide-details="auto"
|
||||
prepend-inner-icon="mdi-text"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Right Column: Preview -->
|
||||
<v-col cols="12" md="6">
|
||||
<div class="h-100">
|
||||
<div class="text-caption font-weight-bold text-medium-emphasis mb-2 ml-1">
|
||||
PERSONA PREVIEW
|
||||
</div>
|
||||
<PersonaQuickPreview
|
||||
:model-value="agent.persona_id"
|
||||
class="h-100"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="cfg.agents.length === 0" class="d-flex flex-column align-center justify-center py-12 text-medium-emphasis">
|
||||
<v-icon icon="mdi-robot-off" size="64" class="mb-4 opacity-50" />
|
||||
<div class="text-h6">No Agents Configured</div>
|
||||
<div class="text-body-2 mb-4">Add a new sub-agent to get started</div>
|
||||
<v-btn color="primary" variant="tonal" @click="addAgent">
|
||||
Create First Agent
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3000" location="top">
|
||||
{{ snackbar.message }}
|
||||
<template #actions>
|
||||
<v-btn variant="text" @click="snackbar.show = false">Close</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -158,9 +252,12 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import ProviderSelector from '@/components/shared/ProviderSelector.vue'
|
||||
import PersonaSelector from '@/components/shared/PersonaSelector.vue'
|
||||
import PersonaQuickPreview from '@/components/shared/PersonaQuickPreview.vue'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
type SubAgentItem = {
|
||||
|
||||
__key: string
|
||||
name: string
|
||||
persona_id: string
|
||||
@@ -196,9 +293,6 @@ const cfg = ref<SubAgentConfig>({
|
||||
agents: []
|
||||
})
|
||||
|
||||
const personaOptions = ref<{ title: string; value: string }[]>([])
|
||||
const personaLoading = ref(false)
|
||||
|
||||
const mainStateDescription = computed(() =>
|
||||
cfg.value.main_enable ? tm('description.enabled') : tm('description.disabled')
|
||||
)
|
||||
@@ -244,24 +338,6 @@ async function loadConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPersonas() {
|
||||
personaLoading.value = true
|
||||
try {
|
||||
const res = await axios.get('/api/persona/list')
|
||||
if (res.data.status === 'ok') {
|
||||
const list = Array.isArray(res.data.data) ? res.data.data : []
|
||||
personaOptions.value = list.map((p: any) => ({
|
||||
title: p.persona_id,
|
||||
value: p.persona_id
|
||||
}))
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(e?.response?.data?.message || tm('messages.loadPersonaFailed'), 'error')
|
||||
} finally {
|
||||
personaLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function addAgent() {
|
||||
cfg.value.agents.push({
|
||||
__key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
||||
@@ -333,7 +409,7 @@ async function save() {
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
await Promise.all([loadConfig(), loadPersonas()])
|
||||
await Promise.all([loadConfig()])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -343,101 +419,21 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.subagent-page {
|
||||
padding: 20px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 40px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.subagent-panel-title {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
.subagent-panels :deep(.v-expansion-panel-text__wrapper) {
|
||||
padding: 16px;
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
|
||||
.subagent-title-left {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.subagent-title-text {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.subagent-title-name {
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.subagent-title-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.72;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
|
||||
.subagent-title-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.subagent-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.subagent-provider {
|
||||
flex: 1;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.subagent-enabled-inline {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
/* Keep the switch compact inside the expansion-panel title row. */
|
||||
.subagent-enabled-inline :deep(.v-input__details) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.subagent-enabled-inline :deep(.v-selection-control) {
|
||||
min-height: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/*
|
||||
Vuetify renders selected chips inside the input control and will grow the
|
||||
field height as chips wrap. For subagent tool assignment this quickly becomes
|
||||
unwieldy, so we cap the chip area height and allow scrolling.
|
||||
|
||||
Note: this must be a non-scoped style so it can reach Vuetify's internal
|
||||
elements.
|
||||
*/
|
||||
.subagent-tools .v-field__input {
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
/* Small breathing room so the scrollbar doesn't overlap chip close icons. */
|
||||
.subagent-tools .v-field__input {
|
||||
padding-right: 6px;
|
||||
.gap-4 {
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user