Merge pull request #6429 from xkeyC/feat/persona_clone
feat: add clone persona functionality
This commit is contained in:
@@ -339,6 +339,41 @@ class PersonaManager:
|
||||
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]:
|
||||
|
||||
@@ -23,6 +23,7 @@ class PersonaRoute(Route):
|
||||
"/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 @@ class PersonaRoute(Route):
|
||||
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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 уже существует"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 已存在"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +299,25 @@ export const usePersonaStore = defineStore({
|
||||
await this.refreshCurrentFolder();
|
||||
},
|
||||
|
||||
/**
|
||||
* 克隆 Persona
|
||||
*/
|
||||
async clonePersona(sourcePersonaId: string, newPersonaId: string): Promise<Persona> {
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量更新排序
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('buttons.edit') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click.stop="$emit('clone')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-content-copy</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('buttons.clone') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click.stop="$emit('move')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-folder-move</v-icon>
|
||||
@@ -97,7 +103,7 @@ export default defineComponent({
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['view', 'edit', 'move', 'delete'],
|
||||
emits: ['view', 'edit', 'clone', 'move', 'delete'],
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/persona');
|
||||
return { tm };
|
||||
|
||||
@@ -79,7 +79,8 @@
|
||||
<v-col v-for="persona in currentPersonas" :key="persona.persona_id" cols="12" sm="6" lg="4"
|
||||
xl="3">
|
||||
<PersonaCard :persona="persona" @view="viewPersona(persona)"
|
||||
@edit="editPersona(persona)" @move="openMovePersonaDialog(persona)"
|
||||
@edit="editPersona(persona)" @clone="openClonePersonaDialog(persona)"
|
||||
@move="openMovePersonaDialog(persona)"
|
||||
@delete="confirmDeletePersona(persona)" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -230,6 +231,33 @@
|
||||
<MoveToFolderDialog v-model="showMoveDialog" :item-type="moveDialogType" :item="moveDialogItem"
|
||||
@moved="showSuccess" @error="showError" />
|
||||
|
||||
<!-- 克隆人格对话框 -->
|
||||
<v-dialog v-model="showCloneDialog" max-width="450px">
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('cloneDialog.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
{{ tm('cloneDialog.description', { name: cloningPersona?.persona_id ?? '' }) }}
|
||||
</p>
|
||||
<v-text-field v-model="cloneNewPersonaId" :label="tm('cloneDialog.newPersonaId')"
|
||||
:hint="tm('cloneDialog.newPersonaIdHint')" persistent-hint variant="outlined"
|
||||
density="comfortable" autofocus
|
||||
:rules="[v => !!v || tm('cloneDialog.validation.required')]"
|
||||
@keyup.enter="submitClonePersona" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="showCloneDialog = false">
|
||||
{{ tm('buttons.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="submitClonePersona" :loading="cloneLoading"
|
||||
:disabled="!cloneNewPersonaId">
|
||||
{{ tm('buttons.clone') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除文件夹确认对话框 -->
|
||||
<v-dialog v-model="showDeleteFolderDialog" max-width="450px">
|
||||
<v-card>
|
||||
@@ -340,6 +368,12 @@ export default defineComponent({
|
||||
moveDialogType: 'persona' as 'persona' | 'folder',
|
||||
moveDialogItem: null as Persona | Folder | null,
|
||||
|
||||
// 克隆对话框
|
||||
showCloneDialog: false,
|
||||
cloningPersona: null as Persona | null,
|
||||
cloneNewPersonaId: '',
|
||||
cloneLoading: false,
|
||||
|
||||
// 消息提示
|
||||
showMessage: false,
|
||||
message: '',
|
||||
@@ -406,7 +440,7 @@ export default defineComponent({
|
||||
await this.initialize();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(usePersonaStore, ['loadFolderTree', 'navigateToFolder', 'updateFolder', 'deleteFolder', 'deletePersona', 'refreshCurrentFolder', 'movePersonaToFolder']),
|
||||
...mapActions(usePersonaStore, ['loadFolderTree', 'navigateToFolder', 'updateFolder', 'deleteFolder', 'deletePersona', 'refreshCurrentFolder', 'movePersonaToFolder', 'clonePersona']),
|
||||
|
||||
async initialize() {
|
||||
await Promise.all([
|
||||
@@ -472,6 +506,27 @@ export default defineComponent({
|
||||
this.showMoveDialog = true;
|
||||
},
|
||||
|
||||
openClonePersonaDialog(persona: Persona) {
|
||||
this.cloningPersona = persona;
|
||||
this.cloneNewPersonaId = `${persona.persona_id}_copy`;
|
||||
this.showCloneDialog = true;
|
||||
},
|
||||
|
||||
async submitClonePersona() {
|
||||
if (!this.cloneNewPersonaId || !this.cloningPersona) return;
|
||||
|
||||
this.cloneLoading = true;
|
||||
try {
|
||||
await this.clonePersona(this.cloningPersona.persona_id, this.cloneNewPersonaId);
|
||||
this.showSuccess(this.tm('cloneDialog.success'));
|
||||
this.showCloneDialog = false;
|
||||
} catch (error: any) {
|
||||
this.showError(error.message || this.tm('cloneDialog.error'));
|
||||
} finally {
|
||||
this.cloneLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async handlePersonaDropped({ persona_id, target_folder_id }: { persona_id: string; target_folder_id: string | null }) {
|
||||
try {
|
||||
await this.movePersonaToFolder(persona_id, target_folder_id);
|
||||
|
||||
Reference in New Issue
Block a user