Compare commits

..

24 Commits

Author SHA1 Message Date
Soulter c0c967390c chore: remove japanese prompt 2026-01-21 16:03:50 +08:00
Soulter aec5f4e9e6 perf: live mode entry 2026-01-21 15:59:24 +08:00
Soulter 991b85e0c0 Merge branch 'master' into feat/live-mode 2026-01-21 15:49:42 +08:00
Ruochen Pan 473d258b69 feat: implement persona folder for advanced persona management (#4443)
* feat(db): add persona folder management for hierarchical organization

Implement hierarchical folder structure for organizing personas:
- Add PersonaFolder model with recursive parent-child relationships
- Add folder_id and sort_order fields to Persona model
- Implement CRUD operations for persona folders in database layer
- Add migration support for existing databases
- Extend PersonaManager with folder management methods
- Add dashboard API routes for folder operations

* feat(persona): add batch sort order update endpoint for personas and folders

Add new API endpoint POST /persona/reorder to batch update sort_order
for both personas and folders. This enables drag-and-drop reordering
in the dashboard UI.

Changes:
- Add abstract batch_update_sort_order method to BaseDatabase
- Implement batch_update_sort_order in SQLiteDatabase
- Add batch_update_sort_order to PersonaManager with cache refresh
- Add reorder_items route handler with input validation

* feat(persona): add folder_id and sort_order params to persona creation

Extend persona creation flow to support folder placement and ordering:
- Add folder_id and sort_order parameters to insert_persona in db layer
- Update PersonaManager.create_persona to accept and pass folder params
- Add get_folder_detail API endpoint for retrieving folder information
- Include folder_id and sort_order in persona creation response
- Add session flush/refresh to return complete persona object

* feat(dashboard): implement persona folder management UI

- Add folder management system with tree view and breadcrumbs
- Implement create, rename, delete, and move operations for folders
- Add drag-and-drop support for organizing personas and folders
- Create new PersonaManager component and Pinia store for state management
- Refactor PersonaPage to support hierarchical structure
- Update locale files with folder-related translations
- Handle empty parent_id correctly in backend route

* feat(dashboard): centralize folder expansion state in persona store

Move folder expansion logic from local component state to global Pinia
store to persist expansion state.
- Add `expandedFolderIds` state and toggle actions to `personaStore`
- Update `FolderTreeNode` to use store state instead of local data
- Automatically navigate to target folder after moving a persona

* feat(dashboard): add reusable folder management component library

Extract folder management UI into reusable base components and create
persona-specific wrapper components that integrate with personaStore.

- Add base folder components (tree, breadcrumb, card, dialogs) with
  customizable labels for i18n support
- Create useFolderManager composable for folder state management
- Implement drag-and-drop support for moving personas between folders
- Add persona-specific wrapper components connecting to personaStore
- Reorganize PersonaManager into views/persona directory structure
- Include comprehensive README documentation for component usage

* refactor(dashboard): remove legacy persona folder management components

Remove deprecated persona folder management Vue components that have been
superseded by the new reusable folder management component library.

Deleted components:
- CreateFolderDialog.vue
- FolderBreadcrumb.vue
- FolderCard.vue
- FolderTree.vue
- FolderTreeNode.vue
- MoveTargetNode.vue
- MoveToFolderDialog.vue
- PersonaCard.vue
- PersonaManager.vue

These components are replaced by the centralized folder management
implementation introduced in commit 3fbb3db2.

* fix(dashboard): add delayed skeleton loading to prevent UI flicker

Implement a 150ms delay before showing the skeleton loader in
PersonaManager to prevent visual flicker during fast loading operations.

- Add showSkeleton state with timer-based delay mechanism
- Use v-fade-transition for smooth skeleton visibility transitions
- Clean up timer on component unmount to prevent memory leaks
- Only display skeleton when loading exceeds threshold duration

* feat(dashboard): add generic folder item selector component for persona selection

Introduce BaseFolderItemSelector.vue as a reusable component for selecting
items within folder hierarchies. Refactor PersonaSelector to use this new
base component instead of its previous flat list implementation.

Changes:
- Add BaseFolderItemSelector with folder tree navigation and item selection
- Extend folder types with SelectableItem and FolderItemSelectorLabels
- Refactor PersonaSelector to leverage the new base component
- Add i18n translations for rootFolder and emptyFolder labels

* feat(persona): add tree-view display for persona list command

Add hierarchical folder tree output for the persona list command,
showing personas organized by folders with visual tree connectors.

- Add _build_tree_output method for recursive tree structure rendering
- Display folders with 📁 icon and personas with 👤 icon
- Show root-level personas separately from folder contents
- Include total persona count in output

* refactor(persona): simplify tree-view output with shorter indentation lines

Replace complex tree connector logic with simpler depth-based indentation
using "│ " prefix. Remove unnecessary parameters (prefix, is_last) and
computed variables (has_content, total_items, item_idx) in favor of a
cleaner depth-based approach.

* feat(dashboard): add duplicate persona ID validation in create form

Add frontend validation to prevent creating personas with duplicate IDs.
Load existing persona IDs when opening the create form and validate
against them in real-time.

- Add existingPersonaIds array and loadExistingPersonaIds method
- Add validation rule to check for duplicate persona IDs
- Add i18n messages for duplicate ID error (en-US and zh-CN)
- Fix minLength validation to require at least 1 character

* i18n(persona): add createButton translation key for folder dialog

Move create button label to folder-specific translation path
instead of using generic buttons.create key.

* feat(persona): show target folder name in persona creation dialog

Add visual feedback showing which folder a new persona will be created in.

- Add info alert in PersonaForm displaying the target folder name
- Pass currentFolderName prop from PersonaManager and PersonaSelector
- Add recursive findFolderName helper to resolve folder ID to name
- Add i18n translations for createInFolder and rootFolder labels

* style:format code

* fix: remove 'persistent' attribute from dialog components

---------

Co-authored-by: Soulter <905617992@qq.com>
2026-01-21 15:45:35 +08:00
jiangman202506 93cc4cebe6 fix: streaming response for DingTalk (#4590)
closes: #4384

* #4384 钉钉消息回复卡片模板

* chore: ruff format

* chore: ruff format

---------

Co-authored-by: ManJiang <man.jiang@jg-robust.com>
Co-authored-by: Soulter <905617992@qq.com>
2026-01-21 15:45:35 +08:00
Clhikari 4d28de6b4a feat: add file drag upload feature for ChatUI (#4583)
* feat(chat): add drag-drop upload and fix batch file upload

* style(chat): adjust drop overlay to only cover input container
2026-01-21 15:45:35 +08:00
Anima-IGCenter e7540b80ad perf: T2I template editor preview (#4574) 2026-01-21 15:45:09 +08:00
Soulter 97ee36b422 fix: ensure embedding dimensions are returned as integers in providers (#4547)
* fix: ensure embedding dimensions are returned as integers in providers

* chore: ruff format
2026-01-21 15:45:09 +08:00
Soulter 242cf8745b chore: bump version to 4.12.3 2026-01-21 15:45:09 +08:00
Soulter 625401a4d0 refactor: update event types for LLM tool usage and response 2026-01-21 15:45:09 +08:00
Soulter c95bbd11ae docs: update 4.12.2 changelog 2026-01-21 15:45:09 +08:00
Soulter 831907b22a chore: bump version to 4.12.2 2026-01-21 15:45:09 +08:00
Soulter ad2dae3a8c fix: clarify logic for skipping initial system messages in conversation 2026-01-21 15:45:09 +08:00
Soulter 92de1061aa feat: skip saving head system messages in history (#4538)
* feat: skip saving the first system message in history

* fix: rename variable for clarity in system message handling

* fix: update logic to skip all system messages until the first non-system message
2026-01-21 15:45:09 +08:00
Soulter ddff652003 chore: update readme
Added '自动压缩对话' feature and updated features list.
2026-01-21 15:45:09 +08:00
Soulter fa4df28c22 feat: nervous 2026-01-18 17:07:19 +08:00
Soulter 06fa7be63e feat: eyes 2026-01-18 10:53:04 +08:00
Soulter e92b103fd0 feat: add metrics 2026-01-17 21:44:13 +08:00
Soulter dcd699d733 feat: enhance live mode audio processing and text handling 2026-01-17 17:11:31 +08:00
Soulter 2e53d8116e feat: genie tts 2026-01-17 16:27:20 +08:00
Soulter 856d3496fa feat: enhance audio processing and metrics display in live mode 2026-01-17 15:35:02 +08:00
Soulter 19e6253d5d feat: metrics 2026-01-17 15:34:46 +08:00
Soulter 1d426a7458 chore: remove 2026-01-17 14:44:36 +08:00
Soulter c0846bc789 feat: astr live 2026-01-17 14:41:05 +08:00
15 changed files with 92 additions and 116 deletions
@@ -11,6 +11,7 @@ from .provider import ProviderCommands
from .setunset import SetUnsetCommands
from .sid import SIDCommand
from .t2i import T2ICommand
from .tool import ToolCommands
from .tts import TTSCommand
__all__ = [
@@ -26,4 +27,5 @@ __all__ = [
"SetUnsetCommands",
"T2ICommand",
"TTSCommand",
"ToolCommands",
]
@@ -0,0 +1,31 @@
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
class ToolCommands:
def __init__(self, context: star.Context):
self.context = context
async def tool_ls(self, event: AstrMessageEvent):
"""查看函数工具列表"""
event.set_result(
MessageEventResult().message("tool 指令在 AstrBot v4.0.0 已经被移除。"),
)
async def tool_on(self, event: AstrMessageEvent, tool_name: str = ""):
"""启用一个函数工具"""
event.set_result(
MessageEventResult().message("tool 指令在 AstrBot v4.0.0 已经被移除。"),
)
async def tool_off(self, event: AstrMessageEvent, tool_name: str = ""):
"""停用一个函数工具"""
event.set_result(
MessageEventResult().message("tool 指令在 AstrBot v4.0.0 已经被移除。"),
)
async def tool_all_off(self, event: AstrMessageEvent):
"""停用所有函数工具"""
event.set_result(
MessageEventResult().message("tool 指令在 AstrBot v4.0.0 已经被移除。"),
)
@@ -13,6 +13,7 @@ from .commands import (
SetUnsetCommands,
SIDCommand,
T2ICommand,
ToolCommands,
TTSCommand,
)
@@ -23,6 +24,7 @@ class Main(star.Star):
self.help_c = HelpCommand(self.context)
self.llm_c = LLMCommands(self.context)
self.tool_c = ToolCommands(self.context)
self.plugin_c = PluginCommands(self.context)
self.admin_c = AdminCommands(self.context)
self.conversation_c = ConversationCommands(self.context)
@@ -45,6 +47,30 @@ class Main(star.Star):
"""开启/关闭 LLM"""
await self.llm_c.llm(event)
@filter.command_group("tool")
def tool(self):
"""函数工具管理"""
@tool.command("ls")
async def tool_ls(self, event: AstrMessageEvent):
"""查看函数工具列表"""
await self.tool_c.tool_ls(event)
@tool.command("on")
async def tool_on(self, event: AstrMessageEvent, tool_name: str):
"""启用一个函数工具"""
await self.tool_c.tool_on(event, tool_name)
@tool.command("off")
async def tool_off(self, event: AstrMessageEvent, tool_name: str):
"""停用一个函数工具"""
await self.tool_c.tool_off(event, tool_name)
@tool.command("off_all")
async def tool_all_off(self, event: AstrMessageEvent):
"""停用所有函数工具"""
await self.tool_c.tool_all_off(event)
@filter.command_group("plugin")
def plugin(self):
"""插件管理"""
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.12.4"
__version__ = "4.12.3"
+1 -3
View File
@@ -56,10 +56,8 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
)
# special handle web_search_tavily
platform_name = run_context.context.event.get_platform_name()
if (
platform_name == "webchat"
and tool.name == "web_search_tavily"
tool.name == "web_search_tavily"
and len(run_context.messages) > 0
and tool_result
and len(tool_result.content)
+2 -16
View File
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.12.4"
VERSION = "4.12.3"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
WEBHOOK_SUPPORTED_PLATFORMS = [
@@ -1191,11 +1191,7 @@ CONFIG_METADATA_2 = {
"type": "genie_tts",
"provider_type": "text_to_speech",
"enable": False,
"genie_character_name": "mika",
"genie_onnx_model_dir": "CharacterModels/v2ProPlus/mika/tts_models",
"genie_language": "Japanese",
"genie_refer_audio_path": "",
"genie_refer_text": "",
"character_name": "mika",
"timeout": 20,
},
"Edge TTS": {
@@ -1414,16 +1410,6 @@ CONFIG_METADATA_2 = {
},
},
"items": {
"genie_onnx_model_dir": {
"description": "ONNX Model Directory",
"type": "string",
"hint": "The directory path containing the ONNX model files",
},
"genie_language": {
"description": "Language",
"type": "string",
"options": ["Japanese", "English", "Chinese"],
},
"provider_source_id": {
"invisible": True,
"type": "string",
@@ -116,12 +116,8 @@ class InternalAgentSubStage(Stage):
if not provider:
logger.error(f"未找到指定的提供商: {sel_provider}")
return provider
try:
prov = _ctx.get_using_provider(umo=event.unified_msg_origin)
except ValueError as e:
logger.error(f"Error occurred while selecting provider: {e}")
return None
return prov
return _ctx.get_using_provider(umo=event.unified_msg_origin)
async def _get_session_conv(self, event: AstrMessageEvent) -> Conversation:
umo = event.unified_msg_origin
@@ -500,7 +496,6 @@ class InternalAgentSubStage(Stage):
try:
provider = self._select_provider(event)
if provider is None:
logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。")
return
if not isinstance(provider, Provider):
logger.error(
@@ -41,7 +41,6 @@ TOOL_CALL_PROMPT = (
"You MUST NOT return an empty response, especially after invoking a tool."
"Before calling any tool, provide a brief explanatory message to the user stating the purpose of the tool call."
"After the tool call is completed, you must briefly summarize the results returned by the tool for the user."
"Keep the role-play and style consistent throughout the conversation."
)
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT = (
@@ -165,6 +165,7 @@ class WakingCheckStage(Stage):
and handler.handler_module_path
== "astrbot.builtin_stars.builtin_commands.main"
):
logger.debug("skipping builtin command")
continue
# filter 需满足 AND 逻辑关系
@@ -62,44 +62,27 @@ class AiocqhttpAdapter(Platform):
@self.bot.on_request()
async def request(event: Event):
try:
abm = await self.convert_message(event)
if not abm:
return
abm = await self.convert_message(event)
if abm:
await self.handle_msg(abm)
except Exception as e:
logger.exception(f"Handle request message failed: {e}")
return
@self.bot.on_notice()
async def notice(event: Event):
try:
abm = await self.convert_message(event)
if abm:
await self.handle_msg(abm)
except Exception as e:
logger.exception(f"Handle notice message failed: {e}")
return
abm = await self.convert_message(event)
if abm:
await self.handle_msg(abm)
@self.bot.on_message("group")
async def group(event: Event):
try:
abm = await self.convert_message(event)
if abm:
await self.handle_msg(abm)
except Exception as e:
logger.exception(f"Handle group message failed: {e}")
return
abm = await self.convert_message(event)
if abm:
await self.handle_msg(abm)
@self.bot.on_message("private")
async def private(event: Event):
try:
abm = await self.convert_message(event)
if abm:
await self.handle_msg(abm)
except Exception as e:
logger.exception(f"Handle private message failed: {e}")
return
abm = await self.convert_message(event)
if abm:
await self.handle_msg(abm)
@self.bot.on_websocket_connection
def on_websocket_connection(_):
@@ -389,10 +372,9 @@ class AiocqhttpAdapter(Platform):
message_str += "".join(at_parts)
elif t == "markdown":
for m in m_group:
text = m["data"].get("markdown") or m["data"].get("content", "")
abm.message.append(Plain(text=text))
message_str += text
text = m["data"].get("markdown") or m["data"].get("content", "")
abm.message.append(Plain(text=text))
message_str += text
else:
for m in m_group:
try:
+2 -16
View File
@@ -29,24 +29,10 @@ class GenieTTSProvider(TTSProvider):
if not genie:
raise ImportError("Please install genie_tts first.")
self.character_name = provider_config.get("genie_character_name", "mika")
language = provider_config.get("genie_language", "Japanese")
model_dir = provider_config.get("genie_onnx_model_dir", "")
refer_audio_path = provider_config.get("genie_refer_audio_path", "")
refer_text = provider_config.get("genie_refer_text", "")
self.character_name = provider_config.get("character_name", "mika")
try:
genie.load_character(
character_name=self.character_name,
language=language,
onnx_model_dir=model_dir,
)
genie.set_reference_audio(
character_name=self.character_name,
audio_path=refer_audio_path,
audio_text=refer_text,
language=language,
)
genie.load_predefined_character(self.character_name)
except Exception as e:
raise RuntimeError(f"Failed to load character {self.character_name}: {e}")
+8 -9
View File
@@ -328,29 +328,28 @@ class Context:
"""获取所有用于 Embedding 任务的 Provider。"""
return self.provider_manager.embedding_provider_insts
def get_using_provider(self, umo: str | None = None) -> Provider | None:
def get_using_provider(self, umo: str | None = None) -> Provider:
"""获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。
Args:
umo: unified_message_origin 值,如果传入并且用户启用了提供商会话隔离,
则使用该会话偏好的对话模型(提供商
则使用该会话偏好的提供商。
Returns:
当前使用的对话模型(提供商),如果未设置则返回 None
当前使用的文本生成提供者
Raises:
ValueError: 该会话来源配置的的对话模型(提供商)的类型不正确
ValueError: 返回的提供者不是 Provider 类型
Note:
通过 /provider 指令可以切换提供者。
"""
prov = self.provider_manager.get_using_provider(
provider_type=ProviderType.CHAT_COMPLETION,
umo=umo,
)
if prov is None:
return None
if not isinstance(prov, Provider):
raise ValueError(
f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}"
)
raise ValueError("返回的 Provider 不是 Provider 类型")
return prov
def get_using_tts_provider(self, umo: str | None = None) -> TTSProvider | None:
-21
View File
@@ -1,21 +0,0 @@
## 更新内容
### 新功能
- 为 ChatUI 添加文件拖拽上传功能 ([#4583](https://github.com/AstrBotDevs/AstrBot/issues/4583))
- 实现人格文件夹以进行高级人格管理 ([#4443](https://github.com/AstrBotDevs/AstrBot/issues/4443))
- 添加人格文件夹管理以支持层级组织 (db)
- 支持 Genie TTS
### 修复
- 增强提供商选择错误处理和日志记录,避免出现 `Provider 不是 Provider 类型` 的错误 ([#4654](https://github.com/AstrBotDevs/AstrBot/issues/4654))
- aiocqhttp 适配器中的 Markdown KeyError 或 UnboundLocalError 问题 ([#4656](https://github.com/AstrBotDevs/AstrBot/issues/4656))
- 确保 providers 中的 embedding 维度作为整数返回 ([#4547](https://github.com/AstrBotDevs/AstrBot/issues/4547))
- 钉钉流式响应问题 ([#4590](https://github.com/AstrBotDevs/AstrBot/issues/4590))
- 提供商选择按钮被长模型名称遮挡的问题 ([#4631](https://github.com/AstrBotDevs/AstrBot/issues/4631))
- 更新 `web_search_tavily` 处理,避免在非 ChatUI 平台出现信息引用 ([#4633](https://github.com/AstrBotDevs/AstrBot/issues/4633))
### 性能优化
- T2I 模板编辑器预览 ([#4574](https://github.com/AstrBotDevs/AstrBot/issues/4574))
### 杂项
- 移除已弃用的 `tool` 命令
@@ -3,7 +3,7 @@
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
{{ tm('providerSelector.notSelected') }}
</span>
<span v-else class="provider-name-text">
<span v-else>
{{ modelValue }}
</span>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
@@ -228,14 +228,6 @@ function closeProviderDrawer() {
</script>
<style scoped>
.provider-name-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100% - 80px);
display: inline-block;
}
.v-list-item {
transition: all 0.2s ease;
}
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.12.4"
version = "4.12.3"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
requires-python = ">=3.10"