feat: add conversation export feature to JSONL for AI training (#4037)

* Initial plan

* Add conversation export functionality (backend and frontend)

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* Address code review feedback: move imports, simplify logic, improve i18n

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* Simplify frontend download logic: remove redundant Blob wrapper and complex filename parsing

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* fix: update conversation export filename format for consistency

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
Copilot
2025-12-14 21:44:12 +08:00
committed by GitHub
parent 16df64c405
commit 65da469deb
4 changed files with 165 additions and 7 deletions

View File

@@ -13,7 +13,8 @@
"refresh": "Refresh"
},
"batch": {
"deleteSelected": "Delete Selected ({count})"
"deleteSelected": "Delete Selected ({count})",
"exportSelected": "Export Selected ({count})"
},
"pagination": {
"itemsPerPage": "Items per page",
@@ -76,7 +77,8 @@
"message": "Are you sure you want to delete the selected {count} conversations? This action cannot be undone, please proceed with caution!",
"andMore": "and {count} more",
"cancel": "Cancel",
"confirm": "Batch Delete"
"confirm": "Batch Delete",
"warning": "Warning: This action cannot be undone!"
}
},
"messages": {
@@ -92,6 +94,9 @@
"noItemSelected": "Please select conversations to delete first",
"batchDeleteSuccess": "Successfully deleted {count} conversations",
"batchDeleteError": "Batch delete failed",
"batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed"
"batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed",
"exportSuccess": "Export successful",
"exportError": "Export failed",
"noItemSelectedForExport": "Please select conversations to export first"
}
}

View File

@@ -13,7 +13,8 @@
"refresh": "刷新"
},
"batch": {
"deleteSelected": "删除选中 ({count})"
"deleteSelected": "删除选中 ({count})",
"exportSelected": "导出选中 ({count})"
},
"pagination": {
"itemsPerPage": "每页",
@@ -76,7 +77,8 @@
"message": "确定要删除选中的 {count} 个对话吗?此操作不可恢复,请谨慎操作!",
"andMore": "等 {count} 个",
"cancel": "取消",
"confirm": "批量删除"
"confirm": "批量删除",
"warning": "警告:此操作不可撤销!"
}
},
"messages": {
@@ -92,6 +94,9 @@
"noItemSelected": "请先选择要删除的对话",
"batchDeleteSuccess": "成功删除 {count} 个对话",
"batchDeleteError": "批量删除失败",
"batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个"
"batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个",
"exportSuccess": "导出成功",
"exportError": "导出失败",
"noItemSelectedForExport": "请先选择要导出的对话"
}
}

View File

@@ -40,6 +40,17 @@
:loading="loading" size="small" class="mr-2">
{{ tm('history.refresh') }}
</v-btn>
<v-btn
v-if="selectedItems.length > 0"
color="success"
prepend-icon="mdi-download"
variant="tonal"
@click="exportConversations"
:disabled="loading"
size="small"
class="mr-2">
{{ tm('batch.exportSelected', { count: selectedItems.length }) }}
</v-btn>
<v-btn
v-if="selectedItems.length > 0"
color="error"
@@ -910,6 +921,53 @@ export default {
}
},
// 导出选中的对话
async exportConversations() {
if (this.selectedItems.length === 0) {
this.showErrorMessage(this.tm('messages.noItemSelectedForExport'));
return;
}
this.loading = true;
try {
// 准备导出的数据
const conversations = this.selectedItems.map(item => ({
user_id: item.user_id,
cid: item.cid
}));
const response = await axios.post('/api/conversation/export', {
conversations: conversations
}, {
responseType: 'blob' // 重要:告诉 axios 响应是一个 blob
});
// 创建一个下载链接
const url = window.URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
// 生成文件名(使用时间戳)
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const filename = `conversations_export_${timestamp}.jsonl`;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
// 清理
link.remove();
window.URL.revokeObjectURL(url);
this.showSuccessMessage(this.tm('messages.exportSuccess'));
} catch (error) {
console.error(this.tm('messages.exportError'), error);
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.exportError'));
} finally {
this.loading = false;
}
},
// 格式化时间戳
formatTimestamp(timestamp) {
if (!timestamp) return this.tm('status.unknown');