mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-01 14:53:07 +08:00
feat(chat): add batch management for conversations
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
| GET | `/sessions` | 获取租户的会话列表 |
|
||||
| PUT | `/sessions/:id` | 更新会话 |
|
||||
| DELETE | `/sessions/:id` | 删除会话 |
|
||||
| DELETE | `/sessions/batch` | 批量删除会话 |
|
||||
| POST | `/sessions/:session_id/generate_title` | 生成会话标题 |
|
||||
| POST | `/sessions/:session_id/stop` | 停止会话 |
|
||||
| GET | `/sessions/continue-stream/:session_id` | 继续未完成的会话 |
|
||||
@@ -315,6 +316,31 @@ curl --location --request DELETE 'http://localhost:8080/api/v1/sessions/411d6b70
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/sessions/batch` - 批量删除会话
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/sessions/batch' \
|
||||
--header 'X-API-Key: sk-vQHV2NZI_LK5W7wHQvH3yGYExX8YnhaHwZipUYbiZKCYJbBQ' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"ids": [
|
||||
"411d6b70-9a85-4d03-bb74-aab0fd8bd12f",
|
||||
"ceb9babb-1e30-41d7-817d-fd584954304b"
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Sessions deleted successfully",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/sessions/:session_id/generate_title` - 生成会话标题
|
||||
|
||||
**请求**:
|
||||
|
||||
@@ -44,6 +44,10 @@ export async function delSession(session_id: string) {
|
||||
return del(`/api/v1/sessions/${session_id}`);
|
||||
}
|
||||
|
||||
export async function batchDelSessions(ids: string[]) {
|
||||
return del(`/api/v1/sessions/batch`, { ids });
|
||||
}
|
||||
|
||||
export async function getSession(session_id: string) {
|
||||
return get(`/api/v1/sessions/${session_id}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="visible" class="batch-overlay" @click.self="handleClose">
|
||||
<div class="batch-modal">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="batch-header">
|
||||
<h2 class="batch-title">{{ t('batchManage.title') }}</h2>
|
||||
<button class="close-btn" @click="handleClose">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<div class="batch-body">
|
||||
<div class="session-list" v-if="sessions.length > 0">
|
||||
<div
|
||||
class="session-item"
|
||||
v-for="item in sessions"
|
||||
:key="item.id"
|
||||
>
|
||||
<t-checkbox
|
||||
:checked="selectedIds.includes(item.id)"
|
||||
@change="toggleSelect(item.id)"
|
||||
/>
|
||||
<div class="session-info" @click="toggleSelect(item.id)">
|
||||
<div class="session-title">{{ item.title || t('menu.newSession') }}</div>
|
||||
<div class="session-time">{{ formatTime(item.updated_at || item.created_at) }}</div>
|
||||
</div>
|
||||
<button class="delete-single-btn" @click="handleDeleteSingle(item)">
|
||||
<t-icon name="delete" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<p>{{ t('menu.newSession') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="batch-footer">
|
||||
<div class="footer-left">
|
||||
<t-checkbox
|
||||
:checked="isAllSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="toggleSelectAll"
|
||||
>
|
||||
{{ t('batchManage.selectAll') }}
|
||||
</t-checkbox>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<t-button theme="default" variant="outline" @click="handleClose">
|
||||
{{ t('batchManage.cancel') }}
|
||||
</t-button>
|
||||
<t-button
|
||||
theme="danger"
|
||||
:disabled="selectedIds.length === 0"
|
||||
:loading="deleting"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
{{ t('batchManage.delete') }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||
import { batchDelSessions, delSession } from '@/api/chat/index'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
sessions: Array<{
|
||||
id: string
|
||||
title: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void
|
||||
(e: 'deleted', ids: string[]): void
|
||||
}>()
|
||||
|
||||
const selectedIds = ref<string[]>([])
|
||||
const deleting = ref(false)
|
||||
|
||||
const isAllSelected = computed(() =>
|
||||
props.sessions.length > 0 && selectedIds.value.length === props.sessions.length
|
||||
)
|
||||
const isIndeterminate = computed(() =>
|
||||
selectedIds.value.length > 0 && selectedIds.value.length < props.sessions.length
|
||||
)
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) selectedIds.value = []
|
||||
})
|
||||
|
||||
const toggleSelect = (id: string) => {
|
||||
const idx = selectedIds.value.indexOf(id)
|
||||
if (idx > -1) {
|
||||
selectedIds.value.splice(idx, 1)
|
||||
} else {
|
||||
selectedIds.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelectAll = (checked: boolean) => {
|
||||
selectedIds.value = checked ? props.sessions.map(s => s.id) : []
|
||||
}
|
||||
|
||||
const formatTime = (dateStr?: string) => {
|
||||
if (!dateStr) return ''
|
||||
const d = new Date(dateStr)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
const handleDeleteSingle = (item: { id: string; title: string }) => {
|
||||
const confirmDialog = DialogPlugin.confirm({
|
||||
header: t('batchManage.deleteConfirmTitle'),
|
||||
body: t('batchManage.deleteConfirmBody', { count: 1 }),
|
||||
confirmBtn: { content: t('common.delete'), theme: 'danger' },
|
||||
cancelBtn: t('common.cancel'),
|
||||
theme: 'warning',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const res: any = await delSession(item.id)
|
||||
if (res && res.success === true) {
|
||||
emit('deleted', [item.id])
|
||||
MessagePlugin.success(t('batchManage.deleteSuccess'))
|
||||
} else {
|
||||
MessagePlugin.error(t('batchManage.deleteFailed'))
|
||||
}
|
||||
} catch {
|
||||
MessagePlugin.error(t('batchManage.deleteFailed'))
|
||||
}
|
||||
confirmDialog.destroy()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedIds.value.length === 0) {
|
||||
MessagePlugin.warning(t('batchManage.noSelection'))
|
||||
return
|
||||
}
|
||||
const confirmDialog = DialogPlugin.confirm({
|
||||
header: t('batchManage.deleteConfirmTitle'),
|
||||
body: t('batchManage.deleteConfirmBody', { count: selectedIds.value.length }),
|
||||
confirmBtn: { content: t('common.delete'), theme: 'danger' },
|
||||
cancelBtn: t('common.cancel'),
|
||||
theme: 'warning',
|
||||
onConfirm: async () => {
|
||||
deleting.value = true
|
||||
try {
|
||||
const ids = [...selectedIds.value]
|
||||
const res: any = await batchDelSessions(ids)
|
||||
if (res && res.success === true) {
|
||||
emit('deleted', ids)
|
||||
selectedIds.value = []
|
||||
MessagePlugin.success(t('batchManage.deleteSuccess'))
|
||||
} else {
|
||||
MessagePlugin.error(t('batchManage.deleteFailed'))
|
||||
}
|
||||
} catch {
|
||||
MessagePlugin.error(t('batchManage.deleteFailed'))
|
||||
}
|
||||
deleting.value = false
|
||||
confirmDialog.destroy()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* 遮罩层 - 与 Settings.vue 保持一致 */
|
||||
.batch-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* 弹窗容器 */
|
||||
.batch-modal {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 70vh;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 6px 28px rgba(15, 23, 42, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.batch-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.batch-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
/* 会话列表区域 */
|
||||
.batch-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 滚动条样式 - 与 Settings 一致 */
|
||||
.batch-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.batch-body::-webkit-scrollbar-track {
|
||||
background: #ffffff;
|
||||
}
|
||||
.batch-body::-webkit-scrollbar-thumb {
|
||||
background: #d0d0d0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.batch-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #b0b0b0;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 14px;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
|
||||
.delete-single-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-top: 4px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.delete-single-btn {
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #999999;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #e34d59;
|
||||
background: rgba(227, 77, 89, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.batch-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 弹窗动画 */
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-enter-active .batch-modal,
|
||||
.modal-leave-active .batch-modal {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-from .batch-modal,
|
||||
.modal-leave-to .batch-modal {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -34,7 +34,7 @@
|
||||
{{ subitem.title }}
|
||||
</span>
|
||||
<t-dropdown
|
||||
:options="[{ content: t('upload.deleteRecord'), value: 'delete' }]"
|
||||
:options="[{ content: t('upload.deleteRecord'), value: 'delete' }, { content: t('menu.batchManage'), value: 'batchManage' }]"
|
||||
@click="handleSessionMenuClick($event, subitem.originalIndex, subitem)"
|
||||
placement="bottom-right"
|
||||
trigger="click">
|
||||
@@ -54,6 +54,13 @@
|
||||
<div class="menu_bottom">
|
||||
<UserMenu />
|
||||
</div>
|
||||
|
||||
<!-- 批量管理对话框 -->
|
||||
<BatchManageDialog
|
||||
v-model:visible="batchManageVisible"
|
||||
:sessions="allSessions"
|
||||
@deleted="handleBatchDeleted"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -72,6 +79,7 @@ import { useUIStore } from '@/stores/ui';
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
import UserMenu from '@/components/UserMenu.vue';
|
||||
import TenantSelector from '@/components/TenantSelector.vue';
|
||||
import BatchManageDialog from '@/components/BatchManageDialog.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -94,6 +102,21 @@ type MenuItem = { title: string; icon: string; path: string; childrenPath?: stri
|
||||
const { menuArr } = storeToRefs(usemenuStore);
|
||||
let activeSubmenu = ref<string>('');
|
||||
|
||||
// 批量管理状态
|
||||
const batchManageVisible = ref(false);
|
||||
|
||||
// 所有会话列表(用于批量管理对话框)
|
||||
const allSessions = computed(() => {
|
||||
const chatMenu = (menuArr.value as unknown as MenuItem[]).find((item: MenuItem) => item.path === 'creatChat');
|
||||
if (!chatMenu || !chatMenu.children) return [];
|
||||
return chatMenu.children.map((s: any) => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
created_at: s.created_at,
|
||||
updated_at: s.updated_at,
|
||||
}));
|
||||
});
|
||||
|
||||
// 是否可以访问所有租户
|
||||
const canAccessAllTenants = computed(() => authStore.canAccessAllTenants);
|
||||
|
||||
@@ -256,6 +279,8 @@ const mouseleaveBotDown = () => {
|
||||
const handleSessionMenuClick = (data: { value: string }, index: number, item: any) => {
|
||||
if (data?.value === 'delete') {
|
||||
delCard(index, item);
|
||||
} else if (data?.value === 'batchManage') {
|
||||
batchManageVisible.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -288,6 +313,24 @@ const delCard = (index: number, item: any) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleBatchDeleted = (ids: string[]) => {
|
||||
const chatMenuItem = (menuArr.value as any[]).find((m: any) => m.path === 'creatChat');
|
||||
if (chatMenuItem && chatMenuItem.children) {
|
||||
const children = chatMenuItem.children;
|
||||
for (const id of ids) {
|
||||
const idx = children.findIndex((s: any) => s.id === id);
|
||||
if (idx !== -1) children.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
total.value = Math.max(0, total.value - ids.length);
|
||||
// 如果当前会话被删除,跳转到创建页
|
||||
const currentChatId = route.params.chatid as string;
|
||||
if (currentChatId && ids.includes(currentChatId)) {
|
||||
router.push('/platform/creatChat');
|
||||
}
|
||||
}
|
||||
|
||||
const debounce = (fn: (...args: any[]) => void, delay: number) => {
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
return (...args: any[]) => {
|
||||
|
||||
@@ -10,10 +10,22 @@ export default {
|
||||
logout: 'Logout',
|
||||
uploadKnowledge: 'Upload Knowledge',
|
||||
deleteRecord: 'Delete Record',
|
||||
batchManage: 'Batch Manage',
|
||||
newSession: 'New Chat',
|
||||
confirmLogout: 'Are you sure you want to logout?',
|
||||
systemInfo: 'System Information'
|
||||
},
|
||||
batchManage: {
|
||||
title: 'Manage Conversations',
|
||||
selectAll: 'Select All',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete Conversations',
|
||||
deleteConfirmTitle: 'Delete Conversations',
|
||||
deleteConfirmBody: 'Are you sure you want to delete the selected {count} conversation(s)? This action cannot be undone.',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteFailed: 'Delete failed, please try again later',
|
||||
noSelection: 'Please select at least one conversation',
|
||||
},
|
||||
listSpaceSidebar: {
|
||||
title: 'Filter',
|
||||
all: 'All',
|
||||
|
||||
@@ -8,10 +8,22 @@ export default {
|
||||
logout: "로그아웃",
|
||||
uploadKnowledge: "지식 업로드",
|
||||
deleteRecord: "기록 삭제",
|
||||
batchManage: "일괄 관리",
|
||||
newSession: "새 세션",
|
||||
confirmLogout: "정말 로그아웃 하시겠습니까?",
|
||||
systemInfo: "시스템 정보",
|
||||
},
|
||||
batchManage: {
|
||||
title: "대화 관리",
|
||||
selectAll: "전체 선택",
|
||||
cancel: "취소",
|
||||
delete: "대화 삭제",
|
||||
deleteConfirmTitle: "대화 삭제",
|
||||
deleteConfirmBody: "선택한 {count}개의 대화를 삭제하시겠습니까? 삭제 후 복구할 수 없습니다.",
|
||||
deleteSuccess: "삭제 성공",
|
||||
deleteFailed: "삭제 실패, 나중에 다시 시도해 주세요",
|
||||
noSelection: "최소 하나의 대화를 선택해 주세요",
|
||||
},
|
||||
knowledgeBase: {
|
||||
title: "지식베이스",
|
||||
list: "지식베이스 목록",
|
||||
|
||||
@@ -8,10 +8,22 @@ export default {
|
||||
logout: 'Выход',
|
||||
uploadKnowledge: 'Загрузить знания',
|
||||
deleteRecord: 'Удалить запись',
|
||||
batchManage: 'Пакетное управление',
|
||||
newSession: 'Новый диалог',
|
||||
confirmLogout: 'Вы уверены, что хотите выйти?',
|
||||
systemInfo: 'Информация о системе'
|
||||
},
|
||||
batchManage: {
|
||||
title: 'Управление диалогами',
|
||||
selectAll: 'Выбрать все',
|
||||
cancel: 'Отмена',
|
||||
delete: 'Удалить диалоги',
|
||||
deleteConfirmTitle: 'Удалить диалоги',
|
||||
deleteConfirmBody: 'Вы уверены, что хотите удалить выбранные {count} диалог(ов)? Это действие необратимо.',
|
||||
deleteSuccess: 'Успешно удалено',
|
||||
deleteFailed: 'Ошибка удаления, попробуйте позже',
|
||||
noSelection: 'Выберите хотя бы один диалог',
|
||||
},
|
||||
knowledgeBase: {
|
||||
title: 'База знаний',
|
||||
list: 'Список баз знаний',
|
||||
|
||||
@@ -10,10 +10,22 @@ export default {
|
||||
logout: "退出登录",
|
||||
uploadKnowledge: "上传知识",
|
||||
deleteRecord: "删除记录",
|
||||
batchManage: "批量管理",
|
||||
newSession: "新会话",
|
||||
confirmLogout: "确定要退出登录吗?",
|
||||
systemInfo: "系统信息",
|
||||
},
|
||||
batchManage: {
|
||||
title: "管理对话记录",
|
||||
selectAll: "全选",
|
||||
cancel: "取消",
|
||||
delete: "删除对话",
|
||||
deleteConfirmTitle: "删除对话",
|
||||
deleteConfirmBody: "确定要删除选中的 {count} 条对话吗?删除后无法恢复。",
|
||||
deleteSuccess: "删除成功",
|
||||
deleteFailed: "删除失败,请稍后再试",
|
||||
noSelection: "请至少选择一条对话",
|
||||
},
|
||||
listSpaceSidebar: {
|
||||
title: "筛选",
|
||||
all: "全部",
|
||||
|
||||
@@ -87,3 +87,11 @@ func (r *sessionRepository) Update(ctx context.Context, session *types.Session)
|
||||
func (r *sessionRepository) Delete(ctx context.Context, tenantID uint64, id string) error {
|
||||
return r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Delete(&types.Session{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
// BatchDelete deletes multiple sessions by IDs
|
||||
func (r *sessionRepository) BatchDelete(ctx context.Context, tenantID uint64, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.WithContext(ctx).Where("tenant_id = ? AND id IN ?", tenantID, ids).Delete(&types.Session{}).Error
|
||||
}
|
||||
|
||||
@@ -225,6 +225,38 @@ func (s *sessionService) DeleteSession(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDeleteSessions deletes multiple sessions by IDs
|
||||
func (s *sessionService) BatchDeleteSessions(ctx context.Context, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
logger.Error(ctx, "Failed to batch delete sessions: IDs list is empty")
|
||||
return errors.New("session ids are required")
|
||||
}
|
||||
|
||||
// Get tenant ID from context
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint64)
|
||||
|
||||
// Cleanup associated resources for each session
|
||||
for _, id := range ids {
|
||||
if err := s.webSearchStateRepo.DeleteWebSearchTempKBState(ctx, id); err != nil {
|
||||
logger.Warnf(ctx, "Failed to cleanup temporary KB for session %s: %v", id, err)
|
||||
}
|
||||
if err := s.sessionStorage.Delete(ctx, id); err != nil {
|
||||
logger.Warnf(ctx, "Failed to cleanup conversation context for session %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Batch delete sessions from repository
|
||||
if err := s.sessionRepo.BatchDelete(ctx, tenantID, ids); err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"session_ids": ids,
|
||||
"tenant_id": tenantID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateTitle generates a title for the current conversation content
|
||||
// modelID: optional model ID to use for title generation (if empty, uses first available KnowledgeQA model)
|
||||
func (s *sessionService) GenerateTitle(ctx context.Context,
|
||||
|
||||
@@ -303,3 +303,56 @@ func (h *Handler) DeleteSession(c *gin.Context) {
|
||||
"message": "Session deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// batchDeleteRequest represents the request body for batch deleting sessions
|
||||
type batchDeleteRequest struct {
|
||||
IDs []string `json:"ids" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// BatchDeleteSessions godoc
|
||||
// @Summary 批量删除会话
|
||||
// @Description 根据ID列表批量删除对话会话
|
||||
// @Tags 会话
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body batchDeleteRequest true "批量删除请求"
|
||||
// @Success 200 {object} map[string]interface{} "删除结果"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /sessions/batch [delete]
|
||||
func (h *Handler) BatchDeleteSessions(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req batchDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Errorf(ctx, "Invalid batch delete request: %v", err)
|
||||
c.Error(errors.NewBadRequestError("invalid request: ids are required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Sanitize all IDs
|
||||
sanitizedIDs := make([]string, 0, len(req.IDs))
|
||||
for _, id := range req.IDs {
|
||||
sanitized := secutils.SanitizeForLog(id)
|
||||
if sanitized != "" {
|
||||
sanitizedIDs = append(sanitizedIDs, sanitized)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sanitizedIDs) == 0 {
|
||||
c.Error(errors.NewBadRequestError("no valid session IDs provided"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.sessionService.BatchDeleteSessions(ctx, sanitizedIDs); err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Sessions deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -266,6 +266,7 @@ func RegisterSessionRoutes(r *gin.RouterGroup, handler *session.Handler) {
|
||||
sessions := r.Group("/sessions")
|
||||
{
|
||||
sessions.POST("", handler.CreateSession)
|
||||
sessions.DELETE("/batch", handler.BatchDeleteSessions)
|
||||
sessions.GET("/:id", handler.GetSession)
|
||||
sessions.GET("", handler.GetSessionsByTenant)
|
||||
sessions.PUT("/:id", handler.UpdateSession)
|
||||
|
||||
@@ -21,6 +21,8 @@ type SessionService interface {
|
||||
UpdateSession(ctx context.Context, session *types.Session) error
|
||||
// DeleteSession deletes a session
|
||||
DeleteSession(ctx context.Context, id string) error
|
||||
// BatchDeleteSessions deletes multiple sessions by IDs
|
||||
BatchDeleteSessions(ctx context.Context, ids []string) error
|
||||
// GenerateTitle generates a title for the current conversation
|
||||
// modelID: optional model ID to use for title generation (if empty, uses first available KnowledgeQA model)
|
||||
GenerateTitle(ctx context.Context, session *types.Session, messages []types.Message, modelID string) (string, error)
|
||||
@@ -79,4 +81,6 @@ type SessionRepository interface {
|
||||
Update(ctx context.Context, session *types.Session) error
|
||||
// Delete deletes a session
|
||||
Delete(ctx context.Context, tenantID uint64, id string) error
|
||||
// BatchDelete deletes multiple sessions by IDs
|
||||
BatchDelete(ctx context.Context, tenantID uint64, ids []string) error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user