Files
AstrBot/astrbot/dashboard/services/persona_service.py

284 lines
10 KiB
Python

from __future__ import annotations
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
from astrbot.core.sentinels import NOT_GIVEN
class PersonaServiceError(Exception):
pass
class PersonaService:
def __init__(self, core_lifecycle: AstrBotCoreLifecycle) -> None:
self.persona_mgr = core_lifecycle.persona_mgr
async def list_personas(
self,
folder_id: str | None,
filter_by_folder: bool,
) -> list[dict]:
if filter_by_folder:
personas = await self.persona_mgr.get_personas_by_folder(
folder_id if folder_id else None
)
else:
personas = await self.persona_mgr.get_all_personas()
return [self.serialize_persona(persona) for persona in personas]
async def get_persona_detail(self, data: object) -> dict:
payload = self._payload(data)
persona_id = payload.get("persona_id")
if not persona_id:
raise PersonaServiceError("缺少必要参数: persona_id")
persona = await self.persona_mgr.get_persona(persona_id)
if not persona:
raise PersonaServiceError("人格不存在")
return self.serialize_persona(persona)
async def create_persona(self, data: object) -> dict:
payload = self._payload(data)
raw_persona_id = payload.get("persona_id")
raw_system_prompt = payload.get("system_prompt")
persona_id = str(raw_persona_id).strip() if raw_persona_id is not None else ""
system_prompt = (
str(raw_system_prompt).strip() if raw_system_prompt is not None else ""
)
begin_dialogs = payload.get("begin_dialogs", [])
tools = payload.get("tools")
skills = payload.get("skills")
custom_error_message = self._normalize_custom_error_message(
payload.get("custom_error_message")
)
folder_id = payload.get("folder_id")
sort_order = payload.get("sort_order", 0)
if not persona_id:
raise PersonaServiceError("人格ID不能为空")
if not system_prompt:
raise PersonaServiceError("系统提示词不能为空")
self._validate_begin_dialogs(begin_dialogs)
persona = await self.persona_mgr.create_persona(
persona_id=persona_id,
system_prompt=system_prompt,
begin_dialogs=begin_dialogs if begin_dialogs else None,
tools=tools if tools else None,
skills=skills if skills else None,
custom_error_message=custom_error_message,
folder_id=folder_id,
sort_order=sort_order,
)
return {
"message": "人格创建成功",
"persona": self.serialize_persona(persona, empty_lists_for_tools=True),
}
async def update_persona(self, data: object) -> dict:
payload = self._payload(data)
persona_id = payload.get("persona_id")
system_prompt = payload.get("system_prompt")
begin_dialogs = payload.get("begin_dialogs")
has_tools = "tools" in payload
tools = payload.get("tools")
has_skills = "skills" in payload
skills = payload.get("skills")
has_custom_error_message = "custom_error_message" in payload
custom_error_message = payload.get("custom_error_message")
if not persona_id:
raise PersonaServiceError("缺少必要参数: persona_id")
if has_custom_error_message:
custom_error_message = self._normalize_custom_error_message(
custom_error_message
)
if begin_dialogs is not None:
self._validate_begin_dialogs(begin_dialogs)
update_kwargs = {
"persona_id": persona_id,
"system_prompt": system_prompt,
"begin_dialogs": begin_dialogs,
}
if has_tools:
update_kwargs["tools"] = tools
if has_skills:
update_kwargs["skills"] = skills
if has_custom_error_message:
update_kwargs["custom_error_message"] = custom_error_message
await self.persona_mgr.update_persona(**update_kwargs)
return {"message": "人格更新成功"}
async def delete_persona(self, data: object) -> dict:
payload = self._payload(data)
persona_id = payload.get("persona_id")
if not persona_id:
raise PersonaServiceError("缺少必要参数: persona_id")
await self.persona_mgr.delete_persona(persona_id)
return {"message": "人格删除成功"}
async def move_persona(self, data: object) -> dict:
payload = self._payload(data)
persona_id = payload.get("persona_id")
folder_id = payload.get("folder_id")
if not persona_id:
raise PersonaServiceError("缺少必要参数: persona_id")
await self.persona_mgr.move_persona_to_folder(persona_id, folder_id)
return {"message": "人格移动成功"}
async def list_folders(self, parent_id: str | None) -> list[dict]:
if parent_id == "":
parent_id = None
folders = await self.persona_mgr.get_folders(parent_id)
return [self.serialize_folder(folder) for folder in folders]
async def get_folder_tree(self):
return await self.persona_mgr.get_folder_tree()
async def get_folder_detail(self, data: object) -> dict:
payload = self._payload(data)
folder_id = payload.get("folder_id")
if not folder_id:
raise PersonaServiceError("缺少必要参数: folder_id")
folder = await self.persona_mgr.get_folder(folder_id)
if not folder:
raise PersonaServiceError("文件夹不存在")
return self.serialize_folder(folder)
async def create_folder(self, data: object) -> dict:
payload = self._payload(data)
name = str(payload.get("name", "")).strip()
parent_id = payload.get("parent_id")
description = payload.get("description")
sort_order = payload.get("sort_order", 0)
if not name:
raise PersonaServiceError("文件夹名称不能为空")
folder = await self.persona_mgr.create_folder(
name=name,
parent_id=parent_id,
description=description,
sort_order=sort_order,
)
return {
"message": "文件夹创建成功",
"folder": self.serialize_folder(folder),
}
async def update_folder(self, data: object) -> dict:
payload = self._payload(data)
folder_id = payload.get("folder_id")
name = payload.get("name")
parent_id = payload.get("parent_id") if "parent_id" in payload else NOT_GIVEN
description = (
payload.get("description") if "description" in payload else NOT_GIVEN
)
sort_order = payload.get("sort_order")
if not folder_id:
raise PersonaServiceError("缺少必要参数: folder_id")
await self.persona_mgr.update_folder(
folder_id=folder_id,
name=name,
parent_id=parent_id,
description=description,
sort_order=sort_order,
)
return {"message": "文件夹更新成功"}
async def delete_folder(self, data: object) -> dict:
payload = self._payload(data)
folder_id = payload.get("folder_id")
if not folder_id:
raise PersonaServiceError("缺少必要参数: folder_id")
await self.persona_mgr.delete_folder(folder_id)
return {"message": "文件夹删除成功"}
async def reorder_items(self, data: object) -> dict:
payload = self._payload(data)
items = payload.get("items", [])
if not items:
raise PersonaServiceError("items 不能为空")
for item in items:
if not all(key in item for key in ("id", "type", "sort_order")):
raise PersonaServiceError(
"每个 item 必须包含 id, type, sort_order 字段"
)
if item["type"] not in ("persona", "folder"):
raise PersonaServiceError("type 字段必须是 'persona''folder'")
await self.persona_mgr.batch_update_sort_order(items)
return {"message": "排序更新成功"}
@staticmethod
def serialize_persona(persona, empty_lists_for_tools: bool = False) -> dict:
return {
"persona_id": persona.persona_id,
"system_prompt": persona.system_prompt,
"begin_dialogs": persona.begin_dialogs or [],
"tools": (persona.tools or []) if empty_lists_for_tools else persona.tools,
"skills": (persona.skills or [])
if empty_lists_for_tools
else persona.skills,
"custom_error_message": persona.custom_error_message,
"folder_id": persona.folder_id,
"sort_order": persona.sort_order,
"created_at": persona.created_at.isoformat()
if persona.created_at
else None,
"updated_at": persona.updated_at.isoformat()
if persona.updated_at
else None,
}
@staticmethod
def serialize_folder(folder) -> dict:
return {
"folder_id": folder.folder_id,
"name": folder.name,
"parent_id": folder.parent_id,
"description": folder.description,
"sort_order": folder.sort_order,
"created_at": folder.created_at.isoformat() if folder.created_at else None,
"updated_at": folder.updated_at.isoformat() if folder.updated_at else None,
}
@staticmethod
def _normalize_custom_error_message(value):
if value is not None:
if not isinstance(value, str):
raise PersonaServiceError("自定义报错回复信息必须是字符串")
return value.strip() or None
return None
@staticmethod
def _validate_begin_dialogs(begin_dialogs) -> None:
if begin_dialogs and len(begin_dialogs) % 2 != 0:
raise PersonaServiceError("预设对话数量必须为偶数(用户和助手轮流对话)")
@staticmethod
def _payload(data: object) -> dict:
return data if isinstance(data, dict) else {}