Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
48 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
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
28 changes: 19 additions & 9 deletions dashboard/src/components/shared/PersonaForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -570,16 +570,26 @@ export default {
}

this.saving = true;
try {
const url = this.editingPersona ? '/api/persona/update' : '/api/persona/create';
const response = await axios.post(url, this.personaForm);

if (response.data.status === 'ok') {
this.$emit('saved', response.data.message || this.tm('messages.saveSuccess'));
this.closeDialog();
} else {
this.$emit('error', response.data.message || this.tm('messages.saveError'));
try {
const url = this.editingPersona ? '/api/persona/update' : '/api/persona/create';

// 白名单过滤字段
const allowedFields = ['persona_id', 'system_prompt', 'begin_dialogs', 'tools'];
Comment thread
Sjshi763 marked this conversation as resolved.
const filteredData = {};
allowedFields.forEach(field => {
if (this.personaForm.hasOwnProperty(field)) {
filteredData[field] = this.personaForm[field];
}
});
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

const response = await axios.post(url, filteredData);
Comment thread
Sjshi763 marked this conversation as resolved.

if (response.data.status === 'ok') {
this.$emit('saved', response.data.message || this.tm('messages.saveSuccess'));
this.closeDialog();
} else {
this.$emit('error', response.data.message || this.tm('messages.saveError'));
}
} catch (error) {
this.$emit('error', error.response?.data?.message || this.tm('messages.saveError'));
}
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/i18n/locales/en-US/features/persona.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"createFirst": "Create First Persona",
"edit": "Edit",
"delete": "Delete",
"export": "Export JSON",
"import": "Import",
"cancel": "Cancel",
"save": "Save",
"move": "Move",
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/i18n/locales/zh-CN/features/persona.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"createFirst": "创建第一个人格",
"edit": "编辑",
"delete": "删除",
"export": "导出 JSON",
"import": "导入",
"cancel": "取消",
"save": "保存",
"move": "移动",
Expand Down
286 changes: 286 additions & 0 deletions dashboard/src/views/PersonaPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,94 @@
{{ tm('page.description') }}
</p>
</div>
<div class="d-flex ga-2">
<v-btn color="secondary" variant="tonal" prepend-icon="mdi-import" @click="triggerImport"
rounded="xl" size="x-large">
{{ tm('buttons.import') || '导入' }}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="mdi-plus" @click="openCreateDialog"
rounded="xl" size="x-large">
{{ tm('buttons.create') }}
</v-btn>
<input type="file" ref="importInput" style="display: none" accept=".json" @change="handleImport">
</div>
</v-row>

<!-- 主容器组件 -->
<PersonaManager />

<!-- 人格卡片网格 -->
<v-row>
<v-col v-for="persona in personas" :key="persona.persona_id" cols="12" md="6" lg="4" xl="3">
<v-card class="persona-card" rounded="md" @click="viewPersona(persona)">
<v-card-title class="d-flex justify-space-between align-center">
<div class="text-truncate ml-2">
{{ persona.persona_id }}
</div>
<v-menu offset-y>
<template v-slot:activator="{ props }">
<v-btn icon="mdi-dots-vertical" variant="text" size="small" v-bind="props"
@click.stop />
</template>
<v-list density="compact">
<v-list-item @click="editPersona(persona)">
<v-list-item-title>
<v-icon class="mr-2" size="small">mdi-pencil</v-icon>
{{ tm('buttons.edit') }}
</v-list-item-title>
</v-list-item>
<v-list-item @click.stop="downloadPersonaJson(persona)">
<v-list-item-title>
<v-icon class="mr-2" size="small">mdi-content-copy</v-icon>
{{ tm('buttons.export') || '导出 JSON' }}
</v-list-item-title>
</v-list-item>
<v-list-item @click="deletePersona(persona)" class="text-error">
<v-list-item-title>
<v-icon class="mr-2" size="small">mdi-delete</v-icon>
{{ tm('buttons.delete') }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-card-title>

<v-card-text>
<div class="system-prompt-preview">
{{ truncateText(persona.system_prompt, 100) }}
</div>

<div class="mt-3" v-if="persona.begin_dialogs && persona.begin_dialogs.length > 0">
<v-chip size="small" color="secondary" variant="tonal" prepend-icon="mdi-chat">
{{ tm('labels.presetDialogs', { count: persona.begin_dialogs.length / 2 }) }}
</v-chip>
</div>

<div class="mt-3 text-caption text-medium-emphasis">
{{ tm('labels.createdAt') }}: {{ formatDate(persona.created_at) }}
</div>
</v-card-text>
</v-card>
</v-col>

<!-- 空状态 -->
<v-col v-if="personas.length === 0 && !loading" cols="12">
<v-card class="text-center pa-8" elevation="0">
<v-icon size="64" color="grey-lighten-1" class="mb-4">mdi-account-group</v-icon>
<h3 class="text-h5 mb-2">{{ tm('empty.title') }}</h3>
<p class="text-body-1 text-medium-emphasis mb-4">{{ tm('empty.description') }}</p>
<v-btn color="primary" variant="tonal" prepend-icon="mdi-plus" @click="openCreateDialog">
{{ tm('buttons.createFirst') }}
</v-btn>
</v-card>
</v-col>
</v-row>

<!-- 加载状态 -->
<v-row v-if="loading">
<v-col v-for="n in 6" :key="n" cols="12" md="6" lg="4" xl="3">
<v-skeleton-loader type="card" rounded="lg"></v-skeleton-loader>
</v-col>
</v-row>

<!-- 主容器组件 -->
Expand All @@ -33,6 +121,204 @@ export default {
const { tm } = useModuleI18n('features/persona');
return { t, tm };
}
};
},
data() {
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
return {
personas: [],
loading: false,
showPersonaDialog: false,
showViewDialog: false,
editingPersona: null,
viewingPersona: null,
showMessage: false,
message: '',
messageType: 'success'
}
},

mounted() {
this.loadPersonas();
},

methods: {
async loadPersonas() {
this.loading = true;
try {
const response = await axios.get('/api/persona/list');
if (response.data.status === 'ok') {
this.personas = response.data.data;
} else {
this.showError(response.data.message || this.tm('messages.loadError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.loadError'));
}
this.loading = false;
},

openCreateDialog() {
this.editingPersona = null;
this.showPersonaDialog = true;
},

editPersona(persona) {
this.editingPersona = persona;
this.showPersonaDialog = true;
},

viewPersona(persona) {
this.viewingPersona = persona;
this.showViewDialog = true;
},

handlePersonaSaved(message) {
this.showSuccess(message);
this.loadPersonas();
},

async deletePersona(persona) {
if (!confirm(this.tm('messages.deleteConfirm', { id: persona.persona_id }))) {
return;
}

try {
const response = await axios.post('/api/persona/delete', {
persona_id: persona.persona_id
});

if (response.data.status === 'ok') {
this.showSuccess(response.data.message || this.tm('messages.deleteSuccess'));
await this.loadPersonas();
} else {
this.showError(response.data.message || this.tm('messages.deleteError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.deleteError'));
}
},

truncateText(text, maxLength) {
if (!text) return '';
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
},

formatDate(dateString) {
if (!dateString) return '';
return new Date(dateString).toLocaleString();
},

showSuccess(message) {
this.message = message;
this.messageType = 'success';
this.showMessage = true;
},

showError(message) {
this.message = message;
this.messageType = 'error';
this.showMessage = true;
},

async downloadPersonaJson(persona) {
try {
// 创建清洁副本,排除系统字段
const cleanPersona = {
persona_id: persona.persona_id,
system_prompt: persona.system_prompt,
begin_dialogs: persona.begin_dialogs,
tools: persona.tools
};

// 格式化 JSON
const jsonString = JSON.stringify(cleanPersona, null, 4);

// 创建 Blob 对象
const blob = new Blob([jsonString], { type: 'application/json' });

// 创建下载链接
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${persona.persona_id}.json`;
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);

// 清理 URL 对象
URL.revokeObjectURL(url);

// 显示成功消息
this.showSuccess(this.tm('messages.downloadSuccess') || 'JSON 文件已下载');
} catch (error) {
// 显示错误消息
this.showError(error.message || this.tm('messages.downloadError') || '下载 JSON 文件失败');
}
},

triggerImport() {
this.$refs.importInput.click();
},

async handleImport(event) {
const file = event.target.files[0];
if (!file) return;

try {
const text = await file.text();
const parsedData = JSON.parse(text);

console.log("Parsed Data:", parsedData);

// 验证必需字段
if (!parsedData.persona_id || !parsedData.system_prompt) {
this.showError('人格 JSON 缺少必需字段喵!');
event.target.value = '';
return;
}

// 检查重复 ID
const id = parsedData.persona_id;
const exists = this.personas.some(persona => persona.persona_id === id);
if (exists) {
this.showError('人格 ID [' + id + '] 已存在喵!');
event.target.value = '';
return;
}

// 白名单过滤字段
const allowedFields = ['persona_id', 'system_prompt', 'begin_dialogs', 'tools'];
const filteredData = {};
allowedFields.forEach(field => {
if (parsedData.hasOwnProperty(field)) {
filteredData[field] = parsedData[field];
}
});

// 调用 API 保存(使用正确的端点)
const response = await axios.post('/api/persona/create', filteredData);

if (response.data.status === 'ok') {
this.showSuccess(response.data.message || '导入成功喵!');
await this.loadPersonas();
} else {
this.showError(response.data.message || '导入失败喵!');
}
} catch (error) {
console.error("Import Error:", error);
if (error instanceof SyntaxError) {
this.showError('JSON 格式错误喵!' + error.message);
} else {
this.showError('导入失败喵!' + (error.response?.data?.message || error.message));
}
}

// 清理文件输入
event.target.value = '';
}
}
};
</script>

Expand Down