Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ba0935d
feat(db): add persona folder management for hierarchical organization
RC-CHN Jan 12, 2026
24a40e4
feat(persona): add batch sort order update endpoint for personas and …
RC-CHN Jan 12, 2026
27e1a72
feat(persona): add folder_id and sort_order params to persona creation
RC-CHN Jan 12, 2026
fd5dc3c
feat(dashboard): implement persona folder management UI
RC-CHN Jan 12, 2026
36355ad
feat(dashboard): centralize folder expansion state in persona store
RC-CHN Jan 12, 2026
3fbb3db
feat(dashboard): add reusable folder management component library
RC-CHN Jan 12, 2026
442ecee
refactor(dashboard): remove legacy persona folder management components
RC-CHN Jan 12, 2026
4243d2f
fix(dashboard): add delayed skeleton loading to prevent UI flicker
RC-CHN Jan 12, 2026
d621e43
feat(dashboard): add generic folder item selector component for perso…
RC-CHN Jan 12, 2026
35f22cd
feat(persona): add tree-view display for persona list command
RC-CHN Jan 12, 2026
92febb7
refactor(persona): simplify tree-view output with shorter indentation…
RC-CHN Jan 12, 2026
c766bf5
feat(dashboard): add duplicate persona ID validation in create form
RC-CHN Jan 12, 2026
b96c1e4
i18n(persona): add createButton translation key for folder dialog
RC-CHN Jan 12, 2026
52a24ee
feat(persona): show target folder name in persona creation dialog
RC-CHN Jan 12, 2026
97964a5
Merge branch 'AstrBotDevs:master' into persona-folder
RC-CHN Jan 13, 2026
70fff5e
style:format code
RC-CHN Jan 13, 2026
f6ea3b7
Merge remote-tracking branch 'origin/master' into persona-folder
Soulter Jan 21, 2026
14296ff
fix: remove 'persistent' attribute from dialog components
Soulter Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 68 additions & 6 deletions astrbot/builtin_stars/builtin_commands/commands/persona.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,55 @@
import builtins
from typing import TYPE_CHECKING

from astrbot.api import sp, star
from astrbot.api.event import AstrMessageEvent, MessageEventResult

if TYPE_CHECKING:
from astrbot.core.db.po import Persona


class PersonaCommands:
def __init__(self, context: star.Context):
self.context = context

def _build_tree_output(
self,
folder_tree: list[dict],
all_personas: list["Persona"],
depth: int = 0,
) -> list[str]:
"""递归构建树状输出,使用短线条表示层级"""
lines: list[str] = []
# 使用短线条作为缩进前缀,每层只用 "│" 加一个空格
prefix = "│ " * depth

for folder in folder_tree:
# 输出文件夹
lines.append(f"{prefix}├ 📁 {folder['name']}/")

# 获取该文件夹下的人格
folder_personas = [
p for p in all_personas if p.folder_id == folder["folder_id"]
]
child_prefix = "│ " * (depth + 1)

# 输出该文件夹下的人格
for persona in folder_personas:
lines.append(f"{child_prefix}├ 👤 {persona.persona_id}")

# 递归处理子文件夹
children = folder.get("children", [])
if children:
lines.extend(
self._build_tree_output(
children,
all_personas,
depth + 1,
)
)

return lines

async def persona(self, message: AstrMessageEvent):
l = message.message_str.split(" ") # noqa: E741
umo = message.unified_msg_origin
Expand Down Expand Up @@ -69,12 +111,32 @@ async def persona(self, message: AstrMessageEvent):
.use_t2i(False),
)
elif l[1] == "list":
parts = ["人格列表:\n"]
for persona in self.context.provider_manager.personas:
parts.append(f"- {persona['name']}\n")
parts.append("\n\n*输入 `/persona view 人格名` 查看人格详细信息")
msg = "".join(parts)
message.set_result(MessageEventResult().message(msg))
# 获取文件夹树和所有人格
folder_tree = await self.context.persona_manager.get_folder_tree()
all_personas = self.context.persona_manager.personas

lines = ["📂 人格列表:\n"]

# 构建树状输出
tree_lines = self._build_tree_output(folder_tree, all_personas)
lines.extend(tree_lines)

# 输出根目录下的人格(没有文件夹的)
root_personas = [p for p in all_personas if p.folder_id is None]
if root_personas:
if tree_lines: # 如果有文件夹内容,加个空行
lines.append("")
for persona in root_personas:
lines.append(f"👤 {persona.persona_id}")

# 统计信息
total_count = len(all_personas)
lines.append(f"\n共 {total_count} 个人格")
lines.append("\n*使用 `/persona <人格名>` 设置人格")
lines.append("*使用 `/persona view <人格名>` 查看详细信息")

msg = "\n".join(lines)
message.set_result(MessageEventResult().message(msg).use_t2i(False))
elif l[1] == "view":
if len(l) == 2:
message.set_result(MessageEventResult().message("请输入人格情景名"))
Expand Down
92 changes: 91 additions & 1 deletion astrbot/core/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
CommandConflict,
ConversationV2,
Persona,
PersonaFolder,
PlatformMessageHistory,
PlatformSession,
PlatformStat,
Expand Down Expand Up @@ -253,8 +254,19 @@ async def insert_persona(
system_prompt: str,
begin_dialogs: list[str] | None = None,
tools: list[str] | None = None,
folder_id: str | None = None,
sort_order: int = 0,
) -> Persona:
"""Insert a new persona record."""
"""Insert a new persona record.

Args:
persona_id: Unique identifier for the persona
system_prompt: System prompt for the persona
begin_dialogs: Optional list of initial dialog strings
tools: Optional list of tool names (None means all tools, [] means no tools)
folder_id: Optional folder ID to place the persona in (None means root)
sort_order: Sort order within the folder (default 0)
"""
...

@abc.abstractmethod
Expand Down Expand Up @@ -283,6 +295,84 @@ async def delete_persona(self, persona_id: str) -> None:
"""Delete a persona by its ID."""
...

# ====
# Persona Folder Management
# ====

@abc.abstractmethod
async def insert_persona_folder(
self,
name: str,
parent_id: str | None = None,
description: str | None = None,
sort_order: int = 0,
) -> PersonaFolder:
"""Insert a new persona folder."""
...

@abc.abstractmethod
async def get_persona_folder_by_id(self, folder_id: str) -> PersonaFolder | None:
"""Get a persona folder by its folder_id."""
...

@abc.abstractmethod
async def get_persona_folders(
self, parent_id: str | None = None
) -> list[PersonaFolder]:
"""Get all persona folders, optionally filtered by parent_id."""
...

@abc.abstractmethod
async def get_all_persona_folders(self) -> list[PersonaFolder]:
"""Get all persona folders."""
...

@abc.abstractmethod
async def update_persona_folder(
self,
folder_id: str,
name: str | None = None,
parent_id: T.Any = None,
description: T.Any = None,
sort_order: int | None = None,
) -> PersonaFolder | None:
"""Update a persona folder."""
...

@abc.abstractmethod
async def delete_persona_folder(self, folder_id: str) -> None:
"""Delete a persona folder by its folder_id."""
...

@abc.abstractmethod
async def move_persona_to_folder(
self, persona_id: str, folder_id: str | None
) -> Persona | None:
"""Move a persona to a folder (or root if folder_id is None)."""
...

@abc.abstractmethod
async def get_personas_by_folder(
self, folder_id: str | None = None
) -> list[Persona]:
"""Get all personas in a specific folder."""
...

@abc.abstractmethod
async def batch_update_sort_order(
self,
items: list[dict],
) -> None:
"""Batch update sort_order for personas and/or folders.

Args:
items: List of dicts with keys:
- id: The persona_id or folder_id
- type: Either "persona" or "folder"
- sort_order: The new sort_order value
"""
...

@abc.abstractmethod
async def insert_preference_or_update(
self,
Expand Down
42 changes: 42 additions & 0 deletions astrbot/core/db/po.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,44 @@ class ConversationV2(SQLModel, table=True):
)


class PersonaFolder(SQLModel, table=True):
"""Persona 文件夹,支持递归层级结构。

用于组织和管理多个 Persona,类似于文件系统的目录结构。
"""

__tablename__: str = "persona_folders"

id: int | None = Field(
primary_key=True,
sa_column_kwargs={"autoincrement": True},
default=None,
)
folder_id: str = Field(
max_length=36,
nullable=False,
unique=True,
default_factory=lambda: str(uuid.uuid4()),
)
name: str = Field(max_length=255, nullable=False)
parent_id: str | None = Field(default=None, max_length=36)
"""父文件夹ID,NULL表示根目录"""
description: str | None = Field(default=None, sa_type=Text)
sort_order: int = Field(default=0)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
)

__table_args__ = (
UniqueConstraint(
"folder_id",
name="uix_persona_folder_id",
),
)


class Persona(SQLModel, table=True):
"""Persona is a set of instructions for LLMs to follow.

Expand All @@ -87,6 +125,10 @@ class Persona(SQLModel, table=True):
"""a list of strings, each representing a dialog to start with"""
tools: list | None = Field(default=None, sa_type=JSON)
"""None means use ALL tools for default, empty list means no tools, otherwise a list of tool names."""
folder_id: str | None = Field(default=None, max_length=36)
"""所属文件夹ID,NULL 表示在根目录"""
sort_order: int = Field(default=0)
"""排序顺序"""
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
Expand Down
Loading