From a9db72c40386ff4c5bae18d1d322e8cb271b8595 Mon Sep 17 00:00:00 2001 From: AstralYang <1723250309@qq.com> Date: Tue, 24 Mar 2026 22:49:01 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(persona):=20=E4=B8=BA=E4=BA=BA?= =?UTF-8?q?=E6=A0=BC=E6=B7=BB=E5=8A=A0=20Subagents=20=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 Persona 数据库模型中新增 subagents 字段,支持选择特定 Subagents - 在仪表板人格表单中添加 Subagents 选择面板,支持搜索和批量选择 - 在人格卡片和详情页中显示 Subagents 配置状态和数量 - 在核心代理逻辑中根据人格配置过滤可用的 Subagents - 更新所有相关 API 接口以支持 subagents 字段的增删改查 - 添加多语言支持(中文、英文、俄文)的 Subagents 相关文本 - 确保数据库迁移兼容性,自动添加 subagents 列到现有表 --- astrbot/core/astr_main_agent.py | 29 ++- astrbot/core/db/__init__.py | 3 + astrbot/core/db/po.py | 4 + astrbot/core/db/sqlite.py | 20 +- astrbot/core/persona_mgr.py | 8 + astrbot/dashboard/routes/persona.py | 9 + .../src/components/shared/PersonaForm.vue | 200 +++++++++++++++++- .../i18n/locales/en-US/features/persona.json | 12 ++ .../i18n/locales/ru-RU/features/persona.json | 12 ++ .../i18n/locales/zh-CN/features/persona.json | 12 ++ dashboard/src/views/persona/PersonaCard.vue | 9 + .../src/views/persona/PersonaManager.vue | 20 ++ 12 files changed, 333 insertions(+), 5 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 2b4a04907e..6671ec628a 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -386,6 +386,23 @@ async def _ensure_persona_and_skills( assigned_tools: set[str] = set() agents = orch_cfg.get("agents", []) + + # 1. 提取白名单 + sub_agents_cfg = (persona or {}).get("subagents") + persona_subagents = ( + {str(name).strip() for name in sub_agents_cfg if str(name).strip()} + if sub_agents_cfg is not None + else None + ) + + # 2. 过滤 agents + if persona_subagents is not None: + agents = [ + agent + for agent in agents + if isinstance(agent, dict) + and str(agent.get("name", "")).strip() in persona_subagents + ] if isinstance(agents, list): for a in agents: if not isinstance(a, dict): @@ -421,8 +438,16 @@ async def _ensure_persona_and_skills( req.func_tool = ToolSet() # add subagent handoff tools - for tool in so.handoffs: - req.func_tool.add_tool(tool) + # 如果 sub_agents_cfg 为 None 或空列表,则默认放行所有 handoffs + if not sub_agents_cfg: + for tool in so.handoffs: + req.func_tool.add_tool(tool) + else: + for tool in so.handoffs: + # 去掉 "transfer_to_" 前缀再匹配 + short_name = tool.name.replace("transfer_to_", "") + if short_name in sub_agents_cfg: + req.func_tool.add_tool(tool) # check duplicates if remove_dup: diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index a18c127ebf..73d72e8fda 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -314,6 +314,7 @@ async def insert_persona( begin_dialogs: list[str] | None = None, tools: list[str] | None = None, skills: list[str] | None = None, + subagents: list[str] | None = None, custom_error_message: str | None = None, folder_id: str | None = None, sort_order: int = 0, @@ -326,6 +327,7 @@ async def insert_persona( begin_dialogs: Optional list of initial dialog strings tools: Optional list of tool names (None means all tools, [] means no tools) skills: Optional list of skill names (None means all skills, [] means no skills) + subagents: Optional list of subagent names (None means all subagents, [] means no subagents) custom_error_message: Optional persona-level fallback error message folder_id: Optional folder ID to place the persona in (None means root) sort_order: Sort order within the folder (default 0) @@ -350,6 +352,7 @@ async def update_persona( begin_dialogs: list[str] | None = None, tools: list[str] | None = None, skills: list[str] | None = None, + subagents: list[str] | None = None, custom_error_message: str | None = None, ) -> Persona | None: """Update a persona's system prompt or begin dialogs.""" diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py index 451f054f62..67ea185bf3 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -126,6 +126,8 @@ class Persona(TimestampMixin, SQLModel, table=True): """None means use ALL tools for default, empty list means no tools, otherwise a list of tool names.""" skills: list | None = Field(default=None, sa_type=JSON) """None means use ALL skills for default, empty list means no skills, otherwise a list of skill names.""" + subagents: list | None = Field(default=None, sa_type=JSON) + """None means use ALL subagents for default, empty list means no subagents, otherwise a list of subagents names.""" custom_error_message: str | None = Field(default=None, sa_type=Text) """Optional custom error message sent to end users when the agent request fails.""" folder_id: str | None = Field(default=None, max_length=36) @@ -474,6 +476,8 @@ class Personality(TypedDict): """工具列表。None 表示使用所有工具,空列表表示不使用任何工具""" skills: list[str] | None """Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills""" + subagents: list[str] | None + """Subagents 列表。None 表示使用所有 Subagents,空列表表示不使用任何 Subagents""" custom_error_message: str | None """可选的人格自定义报错回复信息。配置后将优先发送给最终用户。""" diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index c8e50909d5..3ac1ed8e7d 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -55,9 +55,10 @@ async def initialize(self) -> None: await conn.execute(text("PRAGMA temp_store=MEMORY")) await conn.execute(text("PRAGMA mmap_size=134217728")) await conn.execute(text("PRAGMA optimize")) - # 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容) + # 确保 personas 表有 folder_id、sort_order、skills、subagents 列(前向兼容) await self._ensure_persona_folder_columns(conn) await self._ensure_persona_skills_column(conn) + await self._ensure_persona_subagents_column(conn) await self._ensure_persona_custom_error_message_column(conn) await conn.commit() @@ -93,6 +94,18 @@ async def _ensure_persona_skills_column(self, conn) -> None: if "skills" not in columns: await conn.execute(text("ALTER TABLE personas ADD COLUMN skills JSON")) + async def _ensure_persona_subagents_column(self, conn) -> None: + """确保 personas 表有 subagents 列。 + + 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel + 的 metadata.create_all 自动创建这些列。 + """ + result = await conn.execute(text("PRAGMA table_info(personas)")) + columns = {row[1] for row in result.fetchall()} + + if "subagents" not in columns: + await conn.execute(text("ALTER TABLE personas ADD COLUMN subagents JSON")) + async def _ensure_persona_custom_error_message_column(self, conn) -> None: """确保 personas 表有 custom_error_message 列。""" result = await conn.execute(text("PRAGMA table_info(personas)")) @@ -686,6 +699,7 @@ async def insert_persona( begin_dialogs=None, tools=None, skills=None, + subagents=None, custom_error_message=None, folder_id=None, sort_order=0, @@ -700,6 +714,7 @@ async def insert_persona( begin_dialogs=begin_dialogs or [], tools=tools, skills=skills, + subagents=subagents, custom_error_message=custom_error_message, folder_id=folder_id, sort_order=sort_order, @@ -732,6 +747,7 @@ async def update_persona( begin_dialogs=None, tools=NOT_GIVEN, skills=NOT_GIVEN, + subagents=NOT_GIVEN, custom_error_message=NOT_GIVEN, ): """Update a persona's system prompt or begin dialogs.""" @@ -748,6 +764,8 @@ async def update_persona( values["tools"] = tools if skills is not NOT_GIVEN: values["skills"] = skills + if subagents is not NOT_GIVEN: + values["subagents"] = subagents if custom_error_message is not NOT_GIVEN: values["custom_error_message"] = custom_error_message if not values: diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index 6320ac3bbc..06367144cd 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -141,6 +141,7 @@ async def update_persona( begin_dialogs: list[str] | None = None, tools: list[str] | None | object = NOT_GIVEN, skills: list[str] | None | object = NOT_GIVEN, + subagents: list[str] | None | object = NOT_GIVEN, custom_error_message: str | None | object = NOT_GIVEN, ): """更新指定 persona 的信息。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具""" @@ -152,6 +153,8 @@ async def update_persona( update_kwargs["tools"] = tools if skills is not NOT_GIVEN: update_kwargs["skills"] = skills + if subagents is not NOT_GIVEN: + update_kwargs["subagents"] = subagents if custom_error_message is not NOT_GIVEN: update_kwargs["custom_error_message"] = custom_error_message @@ -319,6 +322,7 @@ async def create_persona( begin_dialogs: list[str] | None = None, tools: list[str] | None = None, skills: list[str] | None = None, + subagents: list[str] | None = None, custom_error_message: str | None = None, folder_id: str | None = None, sort_order: int = 0, @@ -331,6 +335,7 @@ async def create_persona( begin_dialogs: 预设对话列表 tools: 工具列表,None 表示使用所有工具,空列表表示不使用任何工具 skills: Skills 列表,None 表示使用所有 Skills,空列表表示不使用任何 Skills + subagents: Subagents 列表,None 表示使用所有 Subagents,空列表表示不使用任何 Subagents folder_id: 所属文件夹 ID,None 表示根目录 sort_order: 排序顺序 """ @@ -342,6 +347,7 @@ async def create_persona( begin_dialogs, tools=tools, skills=skills, + subagents=subagents, custom_error_message=custom_error_message, folder_id=folder_id, sort_order=sort_order, @@ -369,6 +375,7 @@ def get_v3_persona_data( "mood_imitation_dialogs": [], # deprecated "tools": persona.tools, "skills": persona.skills, + "subagents": persona.subagents, "custom_error_message": persona.custom_error_message, } for persona in self.personas @@ -426,6 +433,7 @@ def get_v3_persona_data( begin_dialogs=selected_default_persona["begin_dialogs"], tools=selected_default_persona["tools"] or None, skills=selected_default_persona["skills"] or None, + subagents=selected_default_persona["subagents"] or None, custom_error_message=selected_default_persona["custom_error_message"], ) diff --git a/astrbot/dashboard/routes/persona.py b/astrbot/dashboard/routes/persona.py index 56c14fe617..e265536248 100644 --- a/astrbot/dashboard/routes/persona.py +++ b/astrbot/dashboard/routes/persona.py @@ -58,6 +58,7 @@ async def list_personas(self): "begin_dialogs": persona.begin_dialogs or [], "tools": persona.tools, "skills": persona.skills, + "subagents": persona.subagents, "custom_error_message": persona.custom_error_message, "folder_id": persona.folder_id, "sort_order": persona.sort_order, @@ -99,6 +100,7 @@ async def get_persona_detail(self): "begin_dialogs": persona.begin_dialogs or [], "tools": persona.tools, "skills": persona.skills, + "subagents": persona.subagents, "custom_error_message": persona.custom_error_message, "folder_id": persona.folder_id, "sort_order": persona.sort_order, @@ -125,6 +127,7 @@ async def create_persona(self): begin_dialogs = data.get("begin_dialogs", []) tools = data.get("tools") skills = data.get("skills") + subagents = data.get("subagents") custom_error_message = data.get("custom_error_message") folder_id = data.get("folder_id") # None 表示根目录 sort_order = data.get("sort_order", 0) @@ -154,6 +157,7 @@ async def create_persona(self): begin_dialogs=begin_dialogs if begin_dialogs else None, tools=tools if tools else None, skills=skills if skills else None, + subagents=subagents if subagents else None, custom_error_message=custom_error_message, folder_id=folder_id, sort_order=sort_order, @@ -170,6 +174,7 @@ async def create_persona(self): "begin_dialogs": persona.begin_dialogs or [], "tools": persona.tools or [], "skills": persona.skills or [], + "subagents": persona.subagents or [], "custom_error_message": persona.custom_error_message, "folder_id": persona.folder_id, "sort_order": persona.sort_order, @@ -201,6 +206,8 @@ async def update_persona(self): tools = data.get("tools") has_skills = "skills" in data skills = data.get("skills") + has_subagents = "subagents" in data + subagents = data.get("subagents") has_custom_error_message = "custom_error_message" in data custom_error_message = data.get("custom_error_message") @@ -232,6 +239,8 @@ async def update_persona(self): update_kwargs["tools"] = tools if has_skills: update_kwargs["skills"] = skills + if has_subagents: + update_kwargs["subagents"] = subagents if has_custom_error_message: update_kwargs["custom_error_message"] = custom_error_message diff --git a/dashboard/src/components/shared/PersonaForm.vue b/dashboard/src/components/shared/PersonaForm.vue index 19865feb6f..8531a125f0 100644 --- a/dashboard/src/components/shared/PersonaForm.vue +++ b/dashboard/src/components/shared/PersonaForm.vue @@ -265,6 +265,101 @@ + + + + mdi-vector-link + {{ tm('form.subagents') }} + + {{ personaForm.subagents.length }} + + + + +
+

+ {{ tm('form.subagentsHelp') }} +

+
+ + + + + + +
+ + +
+ + + +
+ +
+ mdi-lightning-bolt +

{{ tm('form.noSubagentsAvailable') }} +

+
+ +
+ mdi-magnify +

{{ tm('form.noSubagentsFound') }} +

+
+ +
+ +

{{ tm('form.loadingSubagents') }} +

+
+ +
+

+ {{ tm('form.selectedSubagents') }} + + ({{ tm('form.allSelected') }}) + + + ({{ personaForm.subagents.length }}) + +

+
+ + {{ subagentName }} + +
+
+ {{ tm('form.noSubagentsSelected') }} +
+
+
+
+
+ @@ -367,6 +462,9 @@ export default { loadingTools: false, availableSkills: [], loadingSkills: false, + subagentMainEnable:false, + availableSubagents: [], + loadingSubagents: false, existingPersonaIds: [], // 已存在的人格ID列表 personaForm: { persona_id: '', @@ -375,6 +473,7 @@ export default { begin_dialogs: [], tools: [], skills: [], + subagents: [], folder_id: null }, personaIdRules: [ @@ -388,7 +487,9 @@ export default { ], toolSearch: '', skillSearch: '', - skillSelectValue: '0' + skillSelectValue: '0', + subagentSearch: '', + subagentSelectValue: '0' } }, @@ -422,6 +523,16 @@ export default { (skill.description && skill.description.toLowerCase().includes(search)) ); }, + filteredSubagents() { + if (!this.subagentSearch) { + return this.availableSubagents; + } + const search = this.subagentSearch.toLowerCase(); + return this.availableSubagents.filter(subagent => + subagent.name.toLowerCase().includes(search) || + (subagent.public_description && subagent.public_description.toLowerCase().includes(search)) + ); + }, folderDisplayName() { // 优先使用传入的文件夹名称 if (this.currentFolderName) { @@ -450,6 +561,7 @@ export default { this.loadMcpServers(); this.loadTools(); this.loadSkills(); + this.loadSubagents(); } }, editingPersona: { @@ -484,6 +596,15 @@ export default { this.personaForm.skills = []; } } + }, + subagentSelectValue(newValue) { + if (newValue === '0') { + this.personaForm.subagents = null; + } else if (newValue === '1') { + if (this.personaForm.subagents === null) { + this.personaForm.subagents = []; + } + } } }, @@ -496,10 +617,12 @@ export default { begin_dialogs: [], tools: [], skills: [], + subagents: [], folder_id: this.currentFolderId }; this.toolSelectValue = '0'; this.skillSelectValue = '0'; + this.subagentSelectValue = '0' this.expandedPanels = this.getDefaultExpandedPanels(); }, @@ -511,11 +634,13 @@ export default { begin_dialogs: [...(persona.begin_dialogs || [])], tools: persona.tools === null ? null : [...(persona.tools || [])], skills: persona.skills === null ? null : [...(persona.skills || [])], + subagents: persona.subagents === null ? null : [...(persona.subagents || [])], folder_id: persona.folder_id }; // 根据 tools 的值设置 toolSelectValue this.toolSelectValue = persona.tools === null ? '0' : '1'; this.skillSelectValue = persona.skills === null ? '0' : '1'; + this.subagentSelectValue = persona.subagents === null ? '0' : '1'; this.expandedPanels = this.getDefaultExpandedPanels(); }, @@ -581,6 +706,32 @@ export default { } }, + async loadSubagents(){ + this.loadingSubagents = true; + try { + const response = await axios.get('/api/subagent/config'); + if (response.data.status === 'ok') { + const payload = response.data.data || []; + this.subagentMainEnable = payload.main_enable; + if (this.subagentMainEnable){ + if (Array.isArray(payload)) { + this.availableSubagents = payload.filter(subagent => subagent.enabled !== false); + } else { + const subagents = payload.agents || []; + this.availableSubagents = subagents.filter(subagent => subagent.enabled !== false); + } + } + } else { + this.$emit('error', response.data.message || 'Failed to load subagents'); + } + } catch (error) { + this.$emit('error', error.response?.data?.message || 'Failed to load subagents'); + this.availableSubagents = []; + } finally { + this.loadingSubagents = false; + } + }, + async loadExistingPersonaIds() { try { const response = await axios.get('/api/persona/list'); @@ -779,6 +930,38 @@ export default { } }, + toggleSubagent(subagentName) { + if (this.personaForm.subagents === null) { + this.personaForm.subagents = this.availableSubagents.map(subagent => subagent.name) + .filter(name => name !== subagentName); + this.subagentSelectValue = '1'; + } else if (Array.isArray(this.personaForm.subagents)) { + const index = this.personaForm.subagents.indexOf(subagentName); + if (index !== -1) { + this.personaForm.subagents.splice(index, 1); + } else { + this.personaForm.subagents.push(subagentName); + } + } else { + this.personaForm.subagents = [subagentName]; + this.subagentSelectValue = '1'; + } + }, + + removeSubagent(subagentName) { + if (this.personaForm.subagents === null) { + this.personaForm.subagents = this.availableSubagents.map(subagent => subagent.name) + .filter(name => name !== subagentName); + this.subagentSelectValue = '1'; + } else if (Array.isArray(this.personaForm.subagents)) { + const index = this.personaForm.subagents.indexOf(subagentName); + if (index !== -1) { + this.personaForm.subagents.splice(index, 1); + } + } + }, + + truncateText(text, maxLength) { if (!text) return ''; return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; @@ -807,6 +990,13 @@ export default { return Array.isArray(this.personaForm.skills) && this.personaForm.skills.includes(skillName); }, + isSubagentSelected(subagentName) { + if (this.personaForm.subagents === null) { + return true; + } + return Array.isArray(this.personaForm.subagents) && this.personaForm.subagents.includes(subagentName); + }, + isServerSelected(server) { if (!server.tools || server.tools.length === 0) return false; @@ -864,6 +1054,11 @@ export default { overflow-y: auto; } +.subagents-selection { + max-height: 300px; + overflow-y: auto; +} + .v-virtual-scroll { padding-bottom: 16px; } @@ -893,7 +1088,8 @@ export default { } .tools-selection, - .skills-selection { + .skills-selection, + .subagents-selection{ max-height: 38vh; } diff --git a/dashboard/src/i18n/locales/en-US/features/persona.json b/dashboard/src/i18n/locales/en-US/features/persona.json index 84aaef52c6..aa03b7f4ab 100644 --- a/dashboard/src/i18n/locales/en-US/features/persona.json +++ b/dashboard/src/i18n/locales/en-US/features/persona.json @@ -52,6 +52,17 @@ "allSkillsAvailable": "Use all available Skills", "noSkillsSelected": "No skills selected", "skillsRuntimeNoneWarning": "Computer Use runtime is set to None; Skills may not run correctly because no runtime is enabled.", + "subagents": "Subagents Selection", + "subagentsHelp": "Select available Subagents for this persona. Subagents can participate as auxiliary agents in task execution.", + "subagentsAllAvailable": "Use all Subagents by default", + "subagentsSelectSpecific": "Select specific Subagents", + "searchSubagents": "Search Subagents", + "selectedSubagents": "Selected Subagents", + "noSubagentsAvailable": "No Subagents available", + "noSubagentsFound": "No matching Subagents found", + "loadingSubagents": "Loading Subagents...", + "allSubagentsAvailable": "Use all available Subagents", + "noSubagentsSelected": "No Subagents selected", "createInFolder": "Will be created in \"{folder}\"", "rootFolder": "All Personas" }, @@ -88,6 +99,7 @@ "personasTitle": "Personas", "toolsCount": "tools", "skillsCount": "skills", + "subagentsCount": "subagents", "contextMenu": { "moveTo": "Move to..." }, diff --git a/dashboard/src/i18n/locales/ru-RU/features/persona.json b/dashboard/src/i18n/locales/ru-RU/features/persona.json index e6e58ad7fa..ca3a1dcea2 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/persona.json +++ b/dashboard/src/i18n/locales/ru-RU/features/persona.json @@ -52,6 +52,17 @@ "allSkillsAvailable": "Использовать все доступные навыки", "noSkillsSelected": "Навыки не выбраны", "skillsRuntimeNoneWarning": "Среда выполнения Computer Use не задана. Навыки могут не работать, так как нет активного окружения.", + "subagents": "Выбор Subagents", + "subagentsHelp": "Выберите доступных Subagents для этого персонажа. Subagents могут участвовать в выполнении задач в качестве вспомогательных агентов.", + "subagentsAllAvailable": "По умолчанию использовать всех Subagents", + "subagentsSelectSpecific": "Выбрать конкретных Subagents", + "searchSubagents": "Поиск Subagents", + "selectedSubagents": "Выбранные Subagents", + "noSubagentsAvailable": "Нет доступных Subagents", + "noSubagentsFound": "Совпадающие Subagents не найдены", + "loadingSubagents": "Загрузка Subagents...", + "allSubagentsAvailable": "Использовать всех доступных Subagents", + "noSubagentsSelected": "Не выбрано ни одного Subagents", "createInFolder": "Будет создан в папке «{folder}»", "rootFolder": "Все персонажи" }, @@ -88,6 +99,7 @@ "personasTitle": "Персонаж", "toolsCount": "инстр.", "skillsCount": "навыков", + "subagentsCount": "subagents", "contextMenu": { "moveTo": "Переместить в..." }, diff --git a/dashboard/src/i18n/locales/zh-CN/features/persona.json b/dashboard/src/i18n/locales/zh-CN/features/persona.json index d3eec49a57..a92f9dbd25 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/persona.json +++ b/dashboard/src/i18n/locales/zh-CN/features/persona.json @@ -52,6 +52,17 @@ "allSkillsAvailable": "使用所有可用 Skills", "noSkillsSelected": "未选择任何 Skills", "skillsRuntimeNoneWarning": "Computer Use 运行环境为无,Skills 可能无法正确被 Agent 运行,因为没有启用运行环境。", + "subagents": "Subagents 选择", + "subagentsHelp": "为这个人格选择可用的 Subagents。Subagents 可以作为辅助代理参与任务执行。", + "subagentsAllAvailable": "默认使用全部 Subagents", + "subagentsSelectSpecific": "选择指定 Subagents", + "searchSubagents": "搜索 Subagents", + "selectedSubagents": "已选择的 Subagents", + "noSubagentsAvailable": "暂无可用 Subagents", + "noSubagentsFound": "未找到匹配的 Subagents", + "loadingSubagents": "正在加载 Subagents...", + "allSubagentsAvailable": "使用所有可用 Subagents", + "noSubagentsSelected": "未选择任何 Subagents", "createInFolder": "将在「{folder}」中创建", "rootFolder": "全部人格" }, @@ -88,6 +99,7 @@ "personasTitle": "人格", "toolsCount": "个工具", "skillsCount": "个 Skills", + "subagentsCount": "个 Subagents", "contextMenu": { "moveTo": "移动到..." }, diff --git a/dashboard/src/views/persona/PersonaCard.vue b/dashboard/src/views/persona/PersonaCard.vue index 37f523c678..68451e322f 100644 --- a/dashboard/src/views/persona/PersonaCard.vue +++ b/dashboard/src/views/persona/PersonaCard.vue @@ -57,6 +57,14 @@ variant="tonal" prepend-icon="mdi-lightning-bolt"> {{ persona.skills.length }} {{ tm('persona.skillsCount') }} + + {{ tm('form.allSubagentsAvailable') }} + + + {{ persona.subagents.length }} {{ tm('persona.subagentsCount') }} +
@@ -83,6 +91,7 @@ interface Persona { begin_dialogs?: string[] | null; tools?: string[] | null; skills?: string[] | null; + subagents?: string[] | null; created_at?: string; updated_at?: string; folder_id?: string | null; diff --git a/dashboard/src/views/persona/PersonaManager.vue b/dashboard/src/views/persona/PersonaManager.vue index 8ad581779f..621cef7ac7 100644 --- a/dashboard/src/views/persona/PersonaManager.vue +++ b/dashboard/src/views/persona/PersonaManager.vue @@ -191,6 +191,25 @@
+
+

{{ tm('form.subagents') }}

+
+ + {{ tm('form.allSubagentsAvailable') }} + +
+
+ + {{ subagentName }} + +
+
+ {{ tm('form.noSubagentsSelected') }} +
+
+
{{ tm('labels.createdAt') }}: {{ formatDate(viewingPersona.created_at) }}
{{ tm('labels.updatedAt') }}: @@ -290,6 +309,7 @@ interface Persona { begin_dialogs?: string[] | null; tools?: string[] | null; skills?: string[] | null; + subagents?: string[] | null; created_at?: string; updated_at?: string; folder_id?: string | null; From e7e7ba8bf374cd1730a67607de28ef4a154834c1 Mon Sep 17 00:00:00 2001 From: AstralYang <1723250309@qq.com> Date: Tue, 24 Mar 2026 22:59:27 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(persona):=20=E4=B8=BA=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E4=BA=BA=E6=A0=BC=E9=85=8D=E7=BD=AE=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20subagents=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/persona_mgr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index 06367144cd..118a77b7de 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -13,6 +13,7 @@ mood_imitation_dialogs=[], tools=None, skills=None, + subagents=None, custom_error_message=None, _begin_dialogs_processed=[], _mood_imitation_dialogs_processed="", From 4c24fd3b8f5ad25a724c16b475ae772b6314d9ad Mon Sep 17 00:00:00 2001 From: AstralYang <1723250309@qq.com> Date: Tue, 24 Mar 2026 23:59:49 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E5=AD=90=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E5=9B=BE=E6=A0=87=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=AD=90?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E9=85=8D=E7=BD=AE=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 PersonaCard 中的子代理图标从闪电改为链接图标以提升语义一致性 - 移除 PersonaForm 中冗余的子代理主开关检查,直接使用配置中的代理列表 - 修复主代理中子代理白名单处理逻辑,确保名称归一化并正确处理空集合情况 --- astrbot/core/astr_main_agent.py | 27 ++++++++++++------- .../src/components/shared/PersonaForm.vue | 11 ++------ dashboard/src/views/persona/PersonaCard.vue | 4 +-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 6671ec628a..b7b8154bc3 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -387,21 +387,21 @@ async def _ensure_persona_and_skills( assigned_tools: set[str] = set() agents = orch_cfg.get("agents", []) - # 1. 提取白名单 + # 1. 提取白名单(归一化 subagents 名称) sub_agents_cfg = (persona or {}).get("subagents") - persona_subagents = ( + normalized_subagents = ( {str(name).strip() for name in sub_agents_cfg if str(name).strip()} if sub_agents_cfg is not None else None ) - # 2. 过滤 agents - if persona_subagents is not None: + # 2. 过滤 agents(使用归一化后的名称) + if normalized_subagents is not None: agents = [ agent for agent in agents if isinstance(agent, dict) - and str(agent.get("name", "")).strip() in persona_subagents + and str(agent.get("name", "")).strip() in normalized_subagents ] if isinstance(agents, list): for a in agents: @@ -438,15 +438,22 @@ async def _ensure_persona_and_skills( req.func_tool = ToolSet() # add subagent handoff tools - # 如果 sub_agents_cfg 为 None 或空列表,则默认放行所有 handoffs - if not sub_agents_cfg: + # 如果 normalized_subagents 为 None 则默认放行所有 handoffs,空集合禁用所有handoffs + if normalized_subagents is None: + # 不配置 subagents 时,默认放行所有 handoffs for tool in so.handoffs: req.func_tool.add_tool(tool) else: + # 只允许指向归一化白名单中的 subagents 的 handoff for tool in so.handoffs: - # 去掉 "transfer_to_" 前缀再匹配 - short_name = tool.name.replace("transfer_to_", "") - if short_name in sub_agents_cfg: + short_name = ( + (getattr(tool, "name", None) or "") + .replace("transfer_to_", "") + .strip() + ) + if ( + short_name in normalized_subagents + ): # normalized_subagents 可能是空集合则禁用所有 handoffs req.func_tool.add_tool(tool) # check duplicates diff --git a/dashboard/src/components/shared/PersonaForm.vue b/dashboard/src/components/shared/PersonaForm.vue index 8531a125f0..1984539191 100644 --- a/dashboard/src/components/shared/PersonaForm.vue +++ b/dashboard/src/components/shared/PersonaForm.vue @@ -712,15 +712,8 @@ export default { const response = await axios.get('/api/subagent/config'); if (response.data.status === 'ok') { const payload = response.data.data || []; - this.subagentMainEnable = payload.main_enable; - if (this.subagentMainEnable){ - if (Array.isArray(payload)) { - this.availableSubagents = payload.filter(subagent => subagent.enabled !== false); - } else { - const subagents = payload.agents || []; - this.availableSubagents = subagents.filter(subagent => subagent.enabled !== false); - } - } + const subagents = payload.agents || []; + this.availableSubagents = subagents.filter(subagent => subagent.enabled !== false); } else { this.$emit('error', response.data.message || 'Failed to load subagents'); } diff --git a/dashboard/src/views/persona/PersonaCard.vue b/dashboard/src/views/persona/PersonaCard.vue index 68451e322f..66ea15d876 100644 --- a/dashboard/src/views/persona/PersonaCard.vue +++ b/dashboard/src/views/persona/PersonaCard.vue @@ -58,11 +58,11 @@ {{ persona.skills.length }} {{ tm('persona.skillsCount') }} + prepend-icon="mdi-vector-link"> {{ tm('form.allSubagentsAvailable') }} + variant="tonal" prepend-icon="mdi-vector-link"> {{ persona.subagents.length }} {{ tm('persona.subagentsCount') }}
From 87e832903462579db1512464803bedee233f0bec Mon Sep 17 00:00:00 2001 From: AstralYang <1723250309@qq.com> Date: Wed, 25 Mar 2026 00:50:14 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dhandoff=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A3=80=E6=9F=A5=E9=80=BB=E8=BE=91=E4=BB=A5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8agent.name=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 之前的实现错误地尝试从工具名称中提取代理名,现在直接使用工具的agent.name属性进行白名单检查,提升健壮性。 --- astrbot/core/astr_main_agent.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index b7b8154bc3..dc2579348c 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -4,6 +4,7 @@ import copy import datetime import json +import logging import os import platform import zoneinfo @@ -446,14 +447,10 @@ async def _ensure_persona_and_skills( else: # 只允许指向归一化白名单中的 subagents 的 handoff for tool in so.handoffs: - short_name = ( - (getattr(tool, "name", None) or "") - .replace("transfer_to_", "") - .strip() - ) - if ( - short_name in normalized_subagents - ): # normalized_subagents 可能是空集合则禁用所有 handoffs + agent = getattr(tool, "agent", None) + agent_name = getattr(agent, "name", None) if agent else None + logging.info(f"Agent {agent_name}") + if agent_name and agent_name.strip() in normalized_subagents: req.func_tool.add_tool(tool) # check duplicates From 8b564a3b782b49560b8b99d9b459cd86fc05a958 Mon Sep 17 00:00:00 2001 From: AstralYang <1723250309@qq.com> Date: Wed, 25 Mar 2026 01:05:26 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E6=97=A5=E5=BF=97=E4=BB=A5=E6=B8=85=E7=90=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除_ensure_persona_and_skills方法中冗余的调试日志输出,该日志仅用于打印代理名称,不影响功能逻辑。 --- astrbot/core/astr_main_agent.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index dc2579348c..99edc409f1 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -4,7 +4,6 @@ import copy import datetime import json -import logging import os import platform import zoneinfo @@ -449,7 +448,6 @@ async def _ensure_persona_and_skills( for tool in so.handoffs: agent = getattr(tool, "agent", None) agent_name = getattr(agent, "name", None) if agent else None - logging.info(f"Agent {agent_name}") if agent_name and agent_name.strip() in normalized_subagents: req.func_tool.add_tool(tool) From c106f986f0f4e2356106c03a413392165fe3151f Mon Sep 17 00:00:00 2001 From: AstralYang <1723250309@qq.com> Date: Wed, 25 Mar 2026 10:02:18 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AD=90=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E5=B7=A5=E5=85=B7=E5=8C=B9=E9=85=8D=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=B9=B6=E7=A7=BB=E9=99=A4WebUI=E6=9C=AA=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复子代理工具匹配逻辑中可能因agent_name类型是导致的匹配问题 - 移除前端未使用的subagentMainEnable状态变量以简化代码。 --- astrbot/core/astr_main_agent.py | 6 ++++-- dashboard/src/components/shared/PersonaForm.vue | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 99edc409f1..b699455516 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -448,8 +448,10 @@ async def _ensure_persona_and_skills( for tool in so.handoffs: agent = getattr(tool, "agent", None) agent_name = getattr(agent, "name", None) if agent else None - if agent_name and agent_name.strip() in normalized_subagents: - req.func_tool.add_tool(tool) + if agent_name is not None: + name_norm = str(agent_name).strip() + if name_norm and name_norm in normalized_subagents: + req.func_tool.add_tool(tool) # check duplicates if remove_dup: diff --git a/dashboard/src/components/shared/PersonaForm.vue b/dashboard/src/components/shared/PersonaForm.vue index 1984539191..0a81e80972 100644 --- a/dashboard/src/components/shared/PersonaForm.vue +++ b/dashboard/src/components/shared/PersonaForm.vue @@ -462,7 +462,6 @@ export default { loadingTools: false, availableSkills: [], loadingSkills: false, - subagentMainEnable:false, availableSubagents: [], loadingSubagents: false, existingPersonaIds: [], // 已存在的人格ID列表