diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index d141f40e43..63ec70c23f 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -339,6 +339,41 @@ async def create_persona( self.get_v3_persona_data() return new_persona + async def clone_persona( + self, + source_persona_id: str, + new_persona_id: str, + ) -> Persona: + """Clone an existing persona with a new ID. + + Args: + source_persona_id: Source persona ID to clone from + new_persona_id: New persona ID for the clone + + Returns: + The newly created persona clone + """ + source_persona = await self.db.get_persona_by_id(source_persona_id) + if not source_persona: + raise ValueError(f"Persona with ID {source_persona_id} does not exist.") + + if await self.db.get_persona_by_id(new_persona_id): + raise ValueError(f"Persona with ID {new_persona_id} already exists.") + + new_persona = await self.db.insert_persona( + new_persona_id, + source_persona.system_prompt, + source_persona.begin_dialogs, + tools=source_persona.tools, + skills=source_persona.skills, + custom_error_message=source_persona.custom_error_message, + folder_id=source_persona.folder_id, + sort_order=source_persona.sort_order, + ) + self.personas.append(new_persona) + self.get_v3_persona_data() + return new_persona + def get_v3_persona_data( self, ) -> tuple[list[dict], list[Personality], Personality]: diff --git a/astrbot/dashboard/routes/persona.py b/astrbot/dashboard/routes/persona.py index 56c14fe617..31f341fafb 100644 --- a/astrbot/dashboard/routes/persona.py +++ b/astrbot/dashboard/routes/persona.py @@ -23,6 +23,7 @@ def __init__( "/persona/create": ("POST", self.create_persona), "/persona/update": ("POST", self.update_persona), "/persona/delete": ("POST", self.delete_persona), + "/persona/clone": ("POST", self.clone_persona), "/persona/move": ("POST", self.move_persona), "/persona/reorder": ("POST", self.reorder_items), # Folder routes @@ -262,6 +263,55 @@ async def delete_persona(self): logger.error(f"删除人格失败: {e!s}\n{traceback.format_exc()}") return Response().error(f"删除人格失败: {e!s}").__dict__ + async def clone_persona(self): + """克隆人格""" + try: + data = await request.get_json() + source_persona_id = data.get("source_persona_id") + new_persona_id = data.get("new_persona_id", "").strip() + + if not source_persona_id: + return Response().error("缺少必要参数: source_persona_id").__dict__ + + if not new_persona_id: + return Response().error("新人格ID不能为空").__dict__ + + persona = await self.persona_mgr.clone_persona( + source_persona_id=source_persona_id, + new_persona_id=new_persona_id, + ) + + return ( + Response() + .ok( + { + "message": "人格克隆成功", + "persona": { + "persona_id": persona.persona_id, + "system_prompt": persona.system_prompt, + "begin_dialogs": persona.begin_dialogs or [], + "tools": persona.tools or [], + "skills": persona.skills or [], + "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, + }, + }, + ) + .__dict__ + ) + except ValueError as e: + return Response().error(str(e)).__dict__ + except Exception as e: + logger.error(f"克隆人格失败: {e!s}\n{traceback.format_exc()}") + return Response().error(f"克隆人格失败: {e!s}").__dict__ + async def move_persona(self): """移动人格到指定文件夹""" try: diff --git a/dashboard/src/i18n/locales/en-US/features/persona.json b/dashboard/src/i18n/locales/en-US/features/persona.json index 84aaef52c6..7ca100da0b 100644 --- a/dashboard/src/i18n/locales/en-US/features/persona.json +++ b/dashboard/src/i18n/locales/en-US/features/persona.json @@ -10,6 +10,7 @@ "cancel": "Cancel", "save": "Save", "move": "Move", + "clone": "Clone", "addDialogPair": "Add Dialog Pair" }, "labels": { @@ -142,5 +143,17 @@ "description": "Select a destination folder for \"{name}\"", "success": "Moved successfully", "error": "Failed to move" + }, + "cloneDialog": { + "title": "Clone Persona", + "description": "Create a copy of \"{name}\" with a new ID", + "newPersonaId": "New Persona ID", + "newPersonaIdHint": "Enter a unique name for the cloned persona", + "success": "Persona cloned successfully", + "error": "Failed to clone persona", + "validation": { + "required": "Persona ID is required", + "exists": "This persona ID already exists" + } } } diff --git a/dashboard/src/i18n/locales/ru-RU/features/persona.json b/dashboard/src/i18n/locales/ru-RU/features/persona.json index e6e58ad7fa..12dae56148 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/persona.json +++ b/dashboard/src/i18n/locales/ru-RU/features/persona.json @@ -10,6 +10,7 @@ "cancel": "Отмена", "save": "Сохранить", "move": "Переместить", + "clone": "Клонировать", "addDialogPair": "Добавить пример диалога" }, "labels": { @@ -142,5 +143,17 @@ "description": "Выберите папку для «{name}»", "success": "Объект перемещен", "error": "Ошибка перемещения" + }, + "cloneDialog": { + "title": "Клонировать персонажа", + "description": "Создать копию «{name}» с новым ID", + "newPersonaId": "ID нового персонажа", + "newPersonaIdHint": "Введите уникальное имя для клона", + "success": "Персонаж клонирован", + "error": "Ошибка клонирования", + "validation": { + "required": "ID персонажа обязателен", + "exists": "Такой ID уже существует" + } } } \ No newline at end of file diff --git a/dashboard/src/i18n/locales/zh-CN/features/persona.json b/dashboard/src/i18n/locales/zh-CN/features/persona.json index d3eec49a57..adbc331da8 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/persona.json +++ b/dashboard/src/i18n/locales/zh-CN/features/persona.json @@ -10,6 +10,7 @@ "cancel": "取消", "save": "保存", "move": "移动", + "clone": "克隆", "addDialogPair": "添加对话对" }, "labels": { @@ -142,5 +143,17 @@ "description": "为 \"{name}\" 选择目标文件夹", "success": "移动成功", "error": "移动失败" + }, + "cloneDialog": { + "title": "克隆人格", + "description": "为 \"{name}\" 创建一份副本", + "newPersonaId": "新人格 ID", + "newPersonaIdHint": "输入克隆人格的唯一名称", + "success": "人格克隆成功", + "error": "克隆人格失败", + "validation": { + "required": "人格 ID 不能为空", + "exists": "该人格 ID 已存在" + } } } diff --git a/dashboard/src/stores/personaStore.ts b/dashboard/src/stores/personaStore.ts index e27354f1dc..4b5fde5ce6 100644 --- a/dashboard/src/stores/personaStore.ts +++ b/dashboard/src/stores/personaStore.ts @@ -299,6 +299,25 @@ export const usePersonaStore = defineStore({ await this.refreshCurrentFolder(); }, + /** + * 克隆 Persona + */ + async clonePersona(sourcePersonaId: string, newPersonaId: string): Promise { + const response = await axios.post('/api/persona/clone', { + source_persona_id: sourcePersonaId, + new_persona_id: newPersonaId + }); + + if (response.data.status !== 'ok') { + throw new Error(response.data.message || '克隆人格失败'); + } + + // 刷新当前文件夹内容 + await this.refreshCurrentFolder(); + + return response.data.data.persona; + }, + /** * 批量更新排序 */ diff --git a/dashboard/src/views/persona/PersonaCard.vue b/dashboard/src/views/persona/PersonaCard.vue index 37f523c678..0f0c9c7c7e 100644 --- a/dashboard/src/views/persona/PersonaCard.vue +++ b/dashboard/src/views/persona/PersonaCard.vue @@ -14,6 +14,12 @@ {{ tm('buttons.edit') }} + + + {{ tm('buttons.clone') }} +