-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat: import and export personas #4532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2e90c25
59e5a6f
2285695
d1c0122
d2ed543
13461f9
ec3d25c
98e7a9c
125d10a
fd95665
3ec909c
1f75682
92cf0bc
60dfaf0
0e32d62
03bdb51
ebcc15b
e79df4a
d1e7105
fbe0d05
3d3926b
986549b
d3e7104
351ce77
c7eb1b8
edb0166
75e2f99
fbc7b2b
ddddd8a
4667729
987878e
b510616
9132ebb
672dc1d
989c1f5
3075337
b24075f
27ed75d
5c893e0
a953dac
c11a7df
c528c5f
22c1179
4d782b6
9859b7c
586128d
c955bdb
cab99cc
e88001b
88f9bfc
5b97949
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,10 @@ | |
|
|
||
| <!-- 操作按钮组 --> | ||
| <div class="d-flex ga-2"> | ||
| <v-btn variant="outlined" prepend-icon="mdi-upload" @click="triggerImport" | ||
| rounded="lg"> | ||
| {{ tm('buttons.import') }} | ||
| </v-btn> | ||
| <v-btn color="primary" variant="tonal" prepend-icon="mdi-plus" @click="openCreatePersonaDialog" | ||
| rounded="lg"> | ||
| {{ tm('buttons.create') }} | ||
|
|
@@ -36,6 +40,8 @@ | |
| rounded="lg"> | ||
| {{ tm('folder.createButton') }} | ||
| </v-btn> | ||
| <input ref="importFileInput" type="file" accept=".json" style="display: none" | ||
| @change="handleImportFile" /> | ||
| </div> | ||
| </div> | ||
|
|
||
|
|
@@ -80,7 +86,7 @@ | |
| xl="4"> | ||
| <PersonaCard :persona="persona" @view="viewPersona(persona)" | ||
| @edit="editPersona(persona)" @move="openMovePersonaDialog(persona)" | ||
| @delete="confirmDeletePersona(persona)" /> | ||
| @delete="confirmDeletePersona(persona)" @export="handlePersonaExport" /> | ||
| </v-col> | ||
| </v-row> | ||
| </div> | ||
|
|
@@ -267,6 +273,7 @@ | |
| import { defineComponent } from 'vue'; | ||
| import { useI18n, useModuleI18n } from '@/i18n/composables'; | ||
| import { usePersonaStore } from '@/stores/personaStore'; | ||
| import { personaApi } from '@/api/v1'; | ||
| import { mapState, mapActions } from 'pinia'; | ||
|
|
||
| import FolderTree from './FolderTree.vue'; | ||
|
|
@@ -533,6 +540,90 @@ export default defineComponent({ | |
| } | ||
| }, | ||
|
|
||
| // 导出/导入操作 | ||
| handlePersonaExport(message: string) { | ||
| // 根据消息内容判断成功还是失败 | ||
| if (message.includes(this.tm('messages.exportSuccess'))) { | ||
| this.showSuccess(message); | ||
| } else { | ||
| this.showError(message); | ||
| } | ||
| }, | ||
|
Comment on lines
+544
to
+551
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| triggerImport() { | ||
| const input = this.$refs.importFileInput as HTMLInputElement; | ||
| if (input) { | ||
| input.value = ''; // 清空之前的选择,允许重复导入同一文件 | ||
| input.click(); | ||
| } | ||
| }, | ||
|
|
||
| async handleImportFile(event: Event) { | ||
| const input = event.target as HTMLInputElement; | ||
| const file = input.files?.[0]; | ||
| if (!file) return; | ||
|
|
||
| try { | ||
| const text = await file.text(); | ||
| let data: any; | ||
| try { | ||
| data = JSON.parse(text); | ||
| } catch (e) { | ||
| this.showError(this.tm('messages.importFormatError')); | ||
| return; | ||
| } | ||
|
|
||
| // 校验字段 | ||
| if (!data.system_prompt) { | ||
| this.showError(this.tm('messages.importMissingPrompt')); | ||
| return; | ||
| } | ||
|
|
||
| // 检查 persona_id 是否已存在 | ||
| let personaId = data.persona_id || 'imported_persona'; | ||
| const listRes = await personaApi.list(); | ||
| const existingIds = listRes.data.status === 'ok' | ||
| ? (listRes.data.data || []).map((p: any) => p.persona_id) | ||
| : []; | ||
|
|
||
| let renamed = false; | ||
| if (existingIds.includes(personaId)) { | ||
| personaId = `${personaId}_imported`; | ||
| // 如果 _imported 也存在,加数字后缀 | ||
| let counter = 1; | ||
| while (existingIds.includes(personaId)) { | ||
| personaId = `${data.persona_id}_imported_${counter}`; | ||
| counter++; | ||
| } | ||
| renamed = true; | ||
| } | ||
|
Comment on lines
+583
to
+599
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 在导入 Persona 时,如果导入的 JSON 文件中没有 建议使用已解析的 |
||
|
|
||
| // 构造新的人格数据 | ||
| const newPersona = { | ||
| persona_id: personaId, | ||
| system_prompt: data.system_prompt, | ||
| begin_dialogs: data.begin_dialogs || [], | ||
| tools: null, // 默认使用所有工具 | ||
| skills: null, // 默认使用所有 Skills | ||
| folder_id: this.currentFolderId | ||
| }; | ||
|
|
||
| await personaApi.create(newPersona); | ||
|
|
||
| // 刷新列表 | ||
| await this.refreshCurrentFolder(); | ||
|
|
||
| if (renamed) { | ||
| this.showSuccess(this.tm('messages.importIdExists', { id: personaId })); | ||
| } else { | ||
| this.showSuccess(this.tm('messages.importSuccess')); | ||
| } | ||
| } catch (error: any) { | ||
| console.error('导入人格失败:', error); | ||
| this.showError(this.tm('messages.importError', { error: error.message || String(error) })); | ||
| } | ||
| }, | ||
|
|
||
| // 辅助方法 | ||
| formatDate(dateString: string | undefined | null): string { | ||
| if (!dateString) return ''; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
在
PersonaCard.vue中,导出操作完成后会直接将本地化的成功/失败文本作为事件参数 emit 给父组件:this.$emit('export', this.tm('messages.exportSuccess'));而在
PersonaManager.vue中,父组件通过message.includes(this.tm('messages.exportSuccess'))来判断是成功还是失败。这种基于本地化翻译文本进行字符串匹配的设计非常脆弱。如果翻译文本发生改变、包含动态参数,或者在不同语言下格式不同,该判断逻辑很容易失效。
建议将事件参数改为结构化的对象(例如
{ success: boolean; message: string }),使组件间的通信更加健壮和清晰。