From 044b361ac57bc74da01b44ae5fb94ca019645ba0 Mon Sep 17 00:00:00 2001 From: Flartiny <32789998+Flartiny@users.noreply.github.com> Date: Sat, 14 Mar 2026 21:09:36 +0800 Subject: [PATCH] feat: add conversation batch deletion for webchat (#6160) * feat: add conversation batch deletion for webchat * fix: security issues in batch_delete_sessions and better handle batch select * feat: enhance batch selection UI with animated checkbox visibility in ConversationSidebar --------- Co-authored-by: Soulter <905617992@qq.com> --- astrbot/core/db/__init__.py | 7 + astrbot/core/db/sqlite.py | 15 ++ astrbot/dashboard/routes/chat.py | 79 ++++++++-- dashboard/src/components/chat/Chat.vue | 31 ++++ .../components/chat/ConversationSidebar.vue | 142 +++++++++++++++++- dashboard/src/composables/useSessions.ts | 68 +++++++++ .../src/i18n/locales/en-US/features/chat.json | 10 ++ .../src/i18n/locales/zh-CN/features/chat.json | 10 ++ tests/test_dashboard.py | 103 +++++++++++++ 9 files changed, 445 insertions(+), 20 deletions(-) diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 166f770a5..608ecc710 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -647,6 +647,13 @@ class BaseDatabase(abc.ABC): """Get a Platform session by its ID.""" ... + @abc.abstractmethod + async def get_platform_sessions_by_ids( + self, session_ids: list[str] + ) -> list[PlatformSession]: + """Get platform sessions by IDs.""" + ... + @abc.abstractmethod async def get_platform_sessions_by_creator( self, diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index f496e19d5..c8e50909d 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -1417,6 +1417,21 @@ class SQLiteDatabase(BaseDatabase): result = await session.execute(query) return result.scalar_one_or_none() + async def get_platform_sessions_by_ids( + self, session_ids: list[str] + ) -> list[PlatformSession]: + """Get platform sessions by IDs.""" + if not session_ids: + return [] + + async with self.get_db() as session: + session: AsyncSession + query = select(PlatformSession).where( + col(PlatformSession.session_id).in_(session_ids) + ) + result = await session.execute(query) + return list(result.scalars().all()) + async def get_platform_sessions_by_creator( self, creator: str, diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index a914f3cbf..3c888b492 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -51,6 +51,7 @@ class ChatRoute(Route): "/chat/get_session": ("GET", self.get_session), "/chat/stop": ("POST", self.stop_session), "/chat/delete_session": ("GET", self.delete_webchat_session), + "/chat/batch_delete_sessions": ("POST", self.batch_delete_sessions), "/chat/update_session_display_name": ( "POST", self.update_session_display_name, @@ -578,19 +579,9 @@ class ChatRoute(Route): return Response().ok(data={"stopped_count": stopped_count}).__dict__ - async def delete_webchat_session(self): - """Delete a Platform session and all its related data.""" - session_id = request.args.get("session_id") - if not session_id: - return Response().error("Missing key: session_id").__dict__ - username = g.get("username", "guest") - - # 验证会话是否存在且属于当前用户 - session = await self.db.get_platform_session_by_id(session_id) - if not session: - return Response().error(f"Session {session_id} not found").__dict__ - if session.creator != username: - return Response().error("Permission denied").__dict__ + async def _delete_session_internal(self, session, username: str) -> None: + """Delete a single session and all its related data.""" + session_id = session.session_id # 删除该会话下的所有对话 message_type = "GroupMessage" if session.is_group else "FriendMessage" @@ -632,8 +623,70 @@ class ChatRoute(Route): # 删除会话 await self.db.delete_platform_session(session_id) + async def delete_webchat_session(self): + """Delete a Platform session and all its related data.""" + session_id = request.args.get("session_id") + if not session_id: + return Response().error("Missing key: session_id").__dict__ + username = g.get("username", "guest") + + session = await self.db.get_platform_session_by_id(session_id) + if not session: + return Response().error(f"Session {session_id} not found").__dict__ + if session.creator != username: + return Response().error("Permission denied").__dict__ + + await self._delete_session_internal(session, username) + return Response().ok().__dict__ + async def batch_delete_sessions(self): + """Batch delete multiple Platform sessions.""" + post_data = await request.json + if post_data is None: + return Response().error("Missing JSON body").__dict__ + if not isinstance(post_data, dict): + return Response().error("Invalid JSON body: expected object").__dict__ + + session_ids = post_data.get("session_ids") + if not session_ids or not isinstance(session_ids, list): + return Response().error("Missing or invalid key: session_ids").__dict__ + + username = g.get("username", "guest") + sessions = await self.db.get_platform_sessions_by_ids(session_ids) + sessions_by_id = {session.session_id: session for session in sessions} + deleted_count = 0 + failed_items = [] + + for sid in session_ids: + session = sessions_by_id.get(sid) + if not session: + failed_items.append({"session_id": sid, "reason": "not found"}) + continue + if session.creator != username: + failed_items.append({"session_id": sid, "reason": "permission denied"}) + continue + + try: + await self._delete_session_internal(session, username) + deleted_count += 1 + sessions_by_id.pop(sid, None) + except Exception: + logger.warning("Failed to delete session %s", sid) + failed_items.append({"session_id": sid, "reason": "internal_error"}) + + return ( + Response() + .ok( + data={ + "deleted_count": deleted_count, + "failed_count": len(failed_items), + "failed_items": failed_items, + } + ) + .__dict__ + ) + def _extract_attachment_ids(self, history_list) -> list[str]: """从消息历史中提取所有 attachment_id""" attachment_ids = [] diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index 7c25e1bc3..51b2dbd20 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -20,6 +20,7 @@ @selectConversation="handleSelectConversation" @editTitle="showEditTitleDialog" @deleteConversation="handleDeleteConversation" + @batchDeleteConversations="handleBatchDeleteConversations" @closeMobileSidebar="closeMobileSidebar" @toggleTheme="toggleTheme" @toggleFullscreen="toggleFullscreen" @@ -220,6 +221,7 @@ import { useMediaHandling } from '@/composables/useMediaHandling'; import { useProjects } from '@/composables/useProjects'; import type { Project } from '@/components/chat/ProjectList.vue'; import { useRecording } from '@/composables/useRecording'; +import { useToast } from '@/utils/toast'; interface Props { chatboxMode?: boolean; @@ -233,6 +235,7 @@ const router = useRouter(); const route = useRoute(); const { t } = useI18n(); const { tm } = useModuleI18n('features/chat'); +const { warning: toastWarning } = useToast(); const theme = useTheme(); const customizer = useCustomizerStore(); @@ -257,6 +260,7 @@ const { getSessions, newSession, deleteSession: deleteSessionFn, + batchDeleteSessions, showEditTitleDialog, saveTitle, updateSessionTitle, @@ -510,6 +514,33 @@ async function handleDeleteConversation(sessionId: string) { } } +async function handleBatchDeleteConversations(sessionIds: string[]) { + try { + const result = await batchDeleteSessions(sessionIds); + + // 仅在当前会话成功删除时清除信息 + if (result.currentSessionDeleted) { + messages.value = []; + } + + // 失败处理 + if (result.failed_count > 0) { + toastWarning( + tm('batch.partialFailure', { failed: result.failed_count, total: sessionIds.length }) + ); + } + + // 如果在项目视图中,刷新项目会话列表 + if (selectedProjectId.value) { + const sessions = await getProjectSessions(selectedProjectId.value); + projectSessions.value = sessions; + } + } catch (err) { + console.error('Batch delete sessions failed:', err); + toastWarning(tm('batch.requestFailed')); + } +} + async function handleSelectProject(projectId: string) { selectedProjectId.value = projectId; const sessions = await getProjectSessions(projectId); diff --git a/dashboard/src/components/chat/ConversationSidebar.vue b/dashboard/src/components/chat/ConversationSidebar.vue index 37dee4041..a3645694b 100644 --- a/dashboard/src/components/chat/ConversationSidebar.vue +++ b/dashboard/src/components/chat/ConversationSidebar.vue @@ -21,12 +21,31 @@