Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2e90c25
[Feature] 人格设定支持导出/导入。
Sjshi763 Jan 17, 2026
59e5a6f
导入(dev
Sjshi763 Jan 17, 2026
2285695
稍微进化
Sjshi763 Jan 17, 2026
d1c0122
改为直接导出文件
Sjshi763 Jan 17, 2026
d2ed543
删除未使用的i18n键值
Sjshi763 Jan 17, 2026
13461f9
fix:仅白名单字段可以作为文件名
Sjshi763 Jan 17, 2026
ec3d25c
PersonaForm.vue - savePersona 方法添加了白名单字段过滤
Sjshi763 Jan 17, 2026
98e7a9c
Merge branch 'AstrBotDevs:master' into Sjshi763/issue4409
Sjshi763 Jan 18, 2026
125d10a
[Feature] 人格设定支持导出/导入。
Sjshi763 Jan 28, 2026
fd95665
update
Sjshi763 Jan 28, 2026
3ec909c
Merge pull request #1 from Sjshi763/fix-persona-conflict-v2
Sjshi763 Jan 28, 2026
1f75682
[Feature] 人格设定支持导出/导入。
Sjshi763 Jan 28, 2026
92cf0bc
再次完成
Sjshi763 Jan 28, 2026
60dfaf0
修复bug 405
Sjshi763 Jan 28, 2026
0e32d62
移除导入和导出按钮的本地化文本(上一次修改的
Sjshi763 Jan 28, 2026
03bdb51
移动到 persona 的 i18n JSON
Sjshi763 Jan 28, 2026
ebcc15b
Merge branch 'AstrBotDevs:master' into Sjshi763/issue4409
Sjshi763 Jan 30, 2026
e79df4a
Merge branch 'AstrBotDevs:master' into Sjshi763/issue4409
Sjshi763 Feb 2, 2026
d1e7105
[Feature] 人格设定支持导出/导入。
Sjshi763 Feb 2, 2026
fbe0d05
写错地方了,更正i18n
Sjshi763 Feb 2, 2026
3d3926b
保证相同的i18n
Sjshi763 Feb 2, 2026
986549b
在导出时说明不包含tools Skills
Sjshi763 Feb 2, 2026
d3e7104
修改导出格式
Sjshi763 Feb 2, 2026
351ce77
新格式导入
Sjshi763 Feb 2, 2026
c7eb1b8
重复检查
Sjshi763 Feb 2, 2026
edb0166
Merge branch 'AstrBotDevs:master' into Sjshi763/issue4409
Sjshi763 Feb 3, 2026
75e2f99
Merge branch 'AstrBotDevs:master' into Sjshi763/issue4409
Sjshi763 Feb 4, 2026
fbc7b2b
Merge branch 'AstrBotDevs:master' into Sjshi763/issue4409
Sjshi763 Feb 5, 2026
ddddd8a
Merge branch 'AstrBotDevs:master' into Sjshi763/issue4409
Sjshi763 Feb 7, 2026
4667729
Merge branch 'AstrBotDevs:master' into Sjshi763/issue4409
Sjshi763 Feb 10, 2026
987878e
[Bug]当 LLM 的回复本身包含类似 JSON 的格式的时候消息的 content 字段可能被错误地多次序列化
Sjshi763 Jan 9, 2026
b510616
[Bug]当 LLM 的回复本身包含类似 JSON 的格式的时候消息的 content 字段可能被错误地多次序列化
Sjshi763 Jan 17, 2026
9132ebb
[Bug]当 LLM 的回复本身包含类似 JSON 的格式的时候消息的 content 字段可能被错误地多次序列化
Sjshi763 Jan 29, 2026
672dc1d
- 当原始数据是字符串时(`raw_data` 不为 None):返回 `{"type": "json", "data": {"conte…
Sjshi763 Jan 29, 2026
989c1f5
像其他组件一样使用 self.type
Sjshi763 Jan 29, 2026
3075337
在 Json.__init__ 中 传入 self.raw_data
Sjshi763 Jan 29, 2026
b24075f
在 Json.to_dict 中 使用 self.type.lower() 来推导
Sjshi763 Jan 29, 2026
27ed75d
[Bug]当 LLM 的回复本身包含类似 JSON 的格式的时候消息的 content 字段可能被错误地多次序列化
Sjshi763 Jan 9, 2026
5c893e0
[Bug]当 LLM 的回复本身包含类似 JSON 的格式的时候消息的 content 字段可能被错误地多次序列化
Sjshi763 Jan 17, 2026
a953dac
[Bug]当 LLM 的回复本身包含类似 JSON 的格式的时候消息的 content 字段可能被错误地多次序列化
Sjshi763 Jan 29, 2026
c11a7df
- 当原始数据是字符串时(`raw_data` 不为 None):返回 `{"type": "json", "data": {"conte…
Sjshi763 Jan 29, 2026
c528c5f
像其他组件一样使用 self.type
Sjshi763 Jan 29, 2026
22c1179
在 Json.__init__ 中 传入 self.raw_data
Sjshi763 Jan 29, 2026
4d782b6
在 Json.to_dict 中 使用 self.type.lower() 来推导
Sjshi763 Jan 29, 2026
9859b7c
Merge branch 'Sjshi763/issue4363' of https://github.com/Sjshi763/Astr…
Sjshi763 Feb 28, 2026
586128d
[Feature] 人格设定支持导出/导入。
Sjshi763 Apr 4, 2026
c955bdb
Merge branch 'Sjshi763/issue4363' of https://github.com/Sjshi763/Astr…
Sjshi763 May 25, 2026
cab99cc
Merge branch 'master' into Sjshi763/issue4409
Sjshi763 May 25, 2026
e88001b
同步
Sjshi763 Jul 7, 2026
88f9bfc
feat(persona): 添加导入和导出功能,增强用户体验
Sjshi763 Jul 7, 2026
5b97949
Merge branch 'master' into Sjshi763/issue4409
Sjshi763 Jul 7, 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
14 changes: 12 additions & 2 deletions dashboard/src/i18n/locales/en-US/features/persona.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"cancel": "Cancel",
"save": "Save",
"move": "Move",
"addDialogPair": "Add Dialog Pair"
"addDialogPair": "Add Dialog Pair",
"export": "Export",
"import": "Import Persona"
},
"labels": {
"presetDialogs": "Preset Dialogs ({count} pairs)",
Expand Down Expand Up @@ -83,7 +85,15 @@
"saveError": "Save failed",
"deleteConfirm": "Are you sure you want to delete persona \"{id}\"? This action cannot be undone.",
"deleteSuccess": "Deleted successfully",
"deleteError": "Delete failed"
"deleteError": "Delete failed",
"exportConfirm": "Export only includes system prompt and preset dialogs, not tools and Skills config. Continue?",
"exportSuccess": "Exported successfully",
"exportError": "Export failed: {error}",
"importSuccess": "Persona imported successfully, please configure tools and Skills manually",
"importError": "Import failed: {error}",
"importFormatError": "Invalid file format, please select a valid persona export file",
"importMissingPrompt": "Incomplete file content, missing system prompt",
"importIdExists": "Persona name already exists, renamed to \"{id}\""
},
"persona": {
"personasTitle": "Personas",
Expand Down
14 changes: 12 additions & 2 deletions dashboard/src/i18n/locales/ru-RU/features/persona.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"cancel": "Отмена",
"save": "Сохранить",
"move": "Переместить",
"addDialogPair": "Добавить пример диалога"
"addDialogPair": "Добавить пример диалога",
"export": "Экспорт",
"import": "Импорт персонажа"
},
"labels": {
"presetDialogs": "Примеры диалогов ({count})",
Expand Down Expand Up @@ -83,7 +85,15 @@
"saveError": "Ошибка сохранения",
"deleteConfirm": "Вы уверены, что хотите удалить персонажа «{id}»? Это действие необратимо.",
"deleteSuccess": "Удалено",
"deleteError": "Ошибка удаления"
"deleteError": "Ошибка удаления",
"exportConfirm": "Экспорт содержит только системный промпт и примеры диалогов, без инструментов и навыков. Продолжить?",
"exportSuccess": "Экспортировано",
"exportError": "Ошибка экспорта: {error}",
"importSuccess": "Персонаж импортирован, настройте инструменты и навыки вручную",
"importError": "Ошибка импорта: {error}",
"importFormatError": "Неверный формат файла, выберите корректный файл экспорта",
"importMissingPrompt": "Неполное содержимое файла, отсутствует системный промпт",
"importIdExists": "Имя персонажа уже существует, переименовано в «{id}»"
},
"persona": {
"personasTitle": "Персонаж",
Expand Down
14 changes: 12 additions & 2 deletions dashboard/src/i18n/locales/zh-CN/features/persona.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"cancel": "取消",
"save": "保存",
"move": "移动",
"addDialogPair": "添加对话对"
"addDialogPair": "添加对话对",
"export": "导出",
"import": "导入人格"
},
"labels": {
"presetDialogs": "预设对话 ({count} 对)",
Expand Down Expand Up @@ -83,7 +85,15 @@
"saveError": "保存失败",
"deleteConfirm": "确定要删除人格 \"{id}\" 吗?此操作不可撤销。",
"deleteSuccess": "删除成功",
"deleteError": "删除失败"
"deleteError": "删除失败",
"exportConfirm": "导出仅包含系统提示词和预设对话,不包含工具和 Skills 配置。是否继续导出?",
"exportSuccess": "导出成功",
"exportError": "导出失败:{error}",
"importSuccess": "人格导入成功,请手动配置工具和 Skills",
"importError": "导入失败:{error}",
"importFormatError": "文件格式错误,请选择有效的人格导出文件",
"importMissingPrompt": "文件内容不完整,缺少系统提示词",
"importIdExists": "人格名称已存在,已重命名为 \"{id}\""
},
"persona": {
"personasTitle": "人格",
Expand Down
50 changes: 48 additions & 2 deletions dashboard/src/views/persona/PersonaCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
</template>
<v-list-item-title>{{ tm('persona.contextMenu.moveTo') }}</v-list-item-title>
</v-list-item>
<v-list-item @click.stop="exportPersona">
<template v-slot:prepend>
<v-icon size="small">mdi-download</v-icon>
</template>
<v-list-item-title>{{ tm('buttons.export') }}</v-list-item-title>
</v-list-item>
<v-divider class="my-1" />
<v-list-item @click.stop="$emit('delete')" class="text-error">
<template v-slot:prepend>
Expand Down Expand Up @@ -75,6 +81,10 @@
<script lang="ts">
import { defineComponent, type PropType } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import {
askForConfirmation as askForConfirmationDialog,
useConfirmDialog
} from '@/utils/confirmDialog';

interface Persona {
persona_id: string;
Expand All @@ -97,10 +107,11 @@ export default defineComponent({
required: true
}
},
emits: ['view', 'edit', 'move', 'delete'],
emits: ['view', 'edit', 'move', 'delete', 'export'],
setup() {
const { tm } = useModuleI18n('features/persona');
return { tm };
const confirmDialog = useConfirmDialog();
return { tm, confirmDialog };
},
data() {
return {
Expand Down Expand Up @@ -135,6 +146,41 @@ export default defineComponent({
formatDate(dateString: string | undefined | null): string {
if (!dateString) return '';
return new Date(dateString).toLocaleString();
},
async exportPersona() {
// 确认提示:告知用户导出仅包含系统提示词和预设对话
const confirmed = await askForConfirmationDialog(
this.tm('messages.exportConfirm'),
this.confirmDialog,
);
if (!confirmed) return;

try {
// 仅导出 persona_id, system_prompt, begin_dialogs
const exportData = {
persona_id: this.persona.persona_id,
system_prompt: this.persona.system_prompt,
begin_dialogs: this.persona.begin_dialogs || []
};

const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `persona_${this.persona.persona_id}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);

// 通过事件通知父组件显示成功消息
this.$emit('export', this.tm('messages.exportSuccess'));
} catch (error: any) {
console.error('导出人格失败:', error);
// 通过事件通知父组件显示错误消息
this.$emit('export', this.tm('messages.exportError', { error: error.message || String(error) }));
}
Comment on lines +177 to +183

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

PersonaCard.vue 中,导出操作完成后会直接将本地化的成功/失败文本作为事件参数 emit 给父组件:
this.$emit('export', this.tm('messages.exportSuccess'));
而在 PersonaManager.vue 中,父组件通过 message.includes(this.tm('messages.exportSuccess')) 来判断是成功还是失败。

这种基于本地化翻译文本进行字符串匹配的设计非常脆弱。如果翻译文本发生改变、包含动态参数,或者在不同语言下格式不同,该判断逻辑很容易失效。

建议将事件参数改为结构化的对象(例如 { success: boolean; message: string }),使组件间的通信更加健壮和清晰。

                // 通过事件通知父组件显示成功消息
                this.$emit('export', { success: true, message: this.tm('messages.exportSuccess') });
            } catch (error: any) {
                console.error('导出人格失败:', error);
                // 通过事件通知父组件显示错误消息
                this.$emit('export', {
                    success: false,
                    message: this.tm('messages.exportError', { error: error.message || String(error) })
                });
            }

}
}
});
Expand Down
93 changes: 92 additions & 1 deletion dashboard/src/views/persona/PersonaManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}
Expand All @@ -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>

Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

配合 PersonaCard.vue 中将 export 事件参数改为结构化对象的修改,这里也应该相应地更新处理逻辑,直接根据 success 属性来判断成功或失败,避免脆弱的字符串匹配。

        handlePersonaExport(result: { success: boolean; message: string }) {
            if (result.success) {
                this.showSuccess(result.message);
            } else {
                this.showError(result.message);
            }
        },


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在导入 Persona 时,如果导入的 JSON 文件中没有 persona_id 字段(或者为空),代码会默认使用 'imported_persona'。但在处理 ID 冲突的 while 循环中,代码使用了 data.persona_id 来进行拼接(即 data.persona_id + '_imported_' + counter)。这会导致生成类似 undefined_imported_1_imported_1 这样不符合预期的 ID。

建议使用已解析的 baseId 来进行拼接,以确保生成的 ID 正确。

                const baseId = data.persona_id || 'imported_persona';
                let personaId = baseId;
                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 = baseId + '_imported';
                    // 如果 _imported 也存在,加数字后缀
                    let counter = 1;
                    while (existingIds.includes(personaId)) {
                        personaId = baseId + '_imported_' + counter;
                        counter++;
                    }
                    renamed = true;
                }


// 构造新的人格数据
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 '';
Expand Down
Loading