diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index 76112fa60..a6a2710f1 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -87,17 +87,25 @@ class ConversationManager: unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id conversation_id (str): 对话 ID, 是 uuid 格式的字符串 """ - f = False if not conversation_id: conversation_id = self.session_conversations.get(unified_msg_origin) - if conversation_id: - f = True if conversation_id: await self.db.delete_conversation(cid=conversation_id) - if f: + curr_cid = await self.get_curr_conversation_id(unified_msg_origin) + if curr_cid == conversation_id: self.session_conversations.pop(unified_msg_origin, None) await sp.session_remove(unified_msg_origin, "sel_conv_id") + async def delete_conversations_by_user_id(self, unified_msg_origin: str): + """删除会话的所有对话 + + Args: + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + """ + await self.db.delete_conversations_by_user_id(user_id=unified_msg_origin) + self.session_conversations.pop(unified_msg_origin, None) + await sp.session_remove(unified_msg_origin, "sel_conv_id") + async def get_curr_conversation_id(self, unified_msg_origin: str) -> str | None: """获取会话当前的对话 ID diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 2de109b7d..008ef182e 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -154,6 +154,11 @@ class BaseDatabase(abc.ABC): """Delete a conversation by its ID.""" ... + @abc.abstractmethod + async def delete_conversations_by_user_id(self, user_id: str) -> None: + """Delete all conversations for a specific user.""" + ... + @abc.abstractmethod async def insert_platform_message_history( self, diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index 495c4cbaf..51378b017 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -249,6 +249,14 @@ class SQLiteDatabase(BaseDatabase): delete(ConversationV2).where(ConversationV2.conversation_id == cid) ) + async def delete_conversations_by_user_id(self, user_id: str) -> None: + async with self.get_db() as session: + session: AsyncSession + async with session.begin(): + await session.execute( + delete(ConversationV2).where(ConversationV2.user_id == user_id) + ) + async def insert_platform_message_history( self, platform_id, diff --git a/astrbot/core/pipeline/session_status_check/stage.py b/astrbot/core/pipeline/session_status_check/stage.py index ea19e5fc9..3c451e26a 100644 --- a/astrbot/core/pipeline/session_status_check/stage.py +++ b/astrbot/core/pipeline/session_status_check/stage.py @@ -11,7 +11,8 @@ class SessionStatusCheckStage(Stage): """检查会话是否整体启用""" async def initialize(self, ctx: PipelineContext) -> None: - pass + self.ctx = ctx + self.conv_mgr = ctx.plugin_manager.context.conversation_manager async def process( self, event: AstrMessageEvent @@ -19,4 +20,14 @@ class SessionStatusCheckStage(Stage): # 检查会话是否整体启用 if not SessionServiceManager.is_session_enabled(event.unified_msg_origin): logger.debug(f"会话 {event.unified_msg_origin} 已被关闭,已终止事件传播。") + + # workaround for #2309 + conv_id = await self.conv_mgr.get_curr_conversation_id( + event.unified_msg_origin + ) + if not conv_id: + await self.conv_mgr.new_conversation( + event.unified_msg_origin, platform_id=event.get_platform_id() + ) + event.stop_event() diff --git a/astrbot/core/star/session_llm_manager.py b/astrbot/core/star/session_llm_manager.py index 6c5bc994d..8fb88c6b8 100644 --- a/astrbot/core/star/session_llm_manager.py +++ b/astrbot/core/star/session_llm_manager.py @@ -52,10 +52,6 @@ class SessionServiceManager: "session_service_config", session_config, scope="umo", scope_id=session_id ) - logger.info( - f"会话 {session_id} 的LLM状态已更新为: {'启用' if enabled else '禁用'}" - ) - @staticmethod def should_process_llm_request(event: AstrMessageEvent) -> bool: """检查是否应该处理LLM请求 diff --git a/astrbot/dashboard/routes/session_management.py b/astrbot/dashboard/routes/session_management.py index 1271a2493..7a846c52b 100644 --- a/astrbot/dashboard/routes/session_management.py +++ b/astrbot/dashboard/routes/session_management.py @@ -30,6 +30,7 @@ class SessionManagementRoute(Route): "/session/update_tts": ("POST", self.update_session_tts), "/session/update_name": ("POST", self.update_session_name), "/session/update_status": ("POST", self.update_session_status), + "/session/delete": ("POST", self.delete_session), } self.conv_mgr = core_lifecycle.conversation_manager self.core_lifecycle = core_lifecycle @@ -180,60 +181,132 @@ class SessionManagementRoute(Route): logger.error(error_msg) return Response().error(f"获取会话列表失败: {str(e)}").__dict__ + async def _update_single_session_persona(self, session_id: str, persona_name: str): + """更新单个会话的 persona 的内部方法""" + conversation_manager = self.core_lifecycle.star_context.conversation_manager + conversation_id = await conversation_manager.get_curr_conversation_id( + session_id + ) + + conv = None + if conversation_id: + conv = await conversation_manager.get_conversation( + unified_msg_origin=session_id, + conversation_id=conversation_id, + ) + if not conv or not conversation_id: + conversation_id = await conversation_manager.new_conversation(session_id) + + # 更新 persona + await conversation_manager.update_conversation_persona_id( + session_id, persona_name + ) + + async def _handle_batch_operation( + self, session_ids: list, operation_func, operation_name: str, **kwargs + ): + """通用的批量操作处理方法""" + success_count = 0 + error_sessions = [] + + for session_id in session_ids: + try: + await operation_func(session_id, **kwargs) + success_count += 1 + except Exception as e: + logger.error(f"批量{operation_name} 会话 {session_id} 失败: {str(e)}") + error_sessions.append(session_id) + + if error_sessions: + return ( + Response() + .ok( + { + "message": f"批量更新完成,成功: {success_count},失败: {len(error_sessions)}", + "success_count": success_count, + "error_count": len(error_sessions), + "error_sessions": error_sessions, + } + ) + .__dict__ + ) + else: + return ( + Response() + .ok( + { + "message": f"成功批量{operation_name} {success_count} 个会话", + "success_count": success_count, + } + ) + .__dict__ + ) + async def update_session_persona(self): - """更新指定会话的 persona""" + """更新指定会话的 persona,支持批量操作""" try: data = await request.get_json() - session_id = data.get("session_id") + is_batch = data.get("is_batch", False) persona_name = data.get("persona_name") - if not session_id: - return Response().error("缺少必要参数: session_id").__dict__ - if persona_name is None: return Response().error("缺少必要参数: persona_name").__dict__ - # 获取会话当前的对话 ID - conversation_manager = self.core_lifecycle.star_context.conversation_manager - conversation_id = await conversation_manager.get_curr_conversation_id( - session_id - ) + if is_batch: + session_ids = data.get("session_ids", []) + if not session_ids: + return Response().error("缺少必要参数: session_ids").__dict__ - if not conversation_id: - # 如果没有对话,创建一个新的对话 - conversation_id = await conversation_manager.new_conversation( - session_id + return await self._handle_batch_operation( + session_ids, + self._update_single_session_persona, + "更新人格", + persona_name=persona_name, ) + else: + session_id = data.get("session_id") + if not session_id: + return Response().error("缺少必要参数: session_id").__dict__ - # 更新 persona - await conversation_manager.update_conversation_persona_id( - session_id, persona_name - ) - - return ( - Response() - .ok({"message": f"成功更新会话 {session_id} 的人格为 {persona_name}"}) - .__dict__ - ) + await self._update_single_session_persona(session_id, persona_name) + return ( + Response() + .ok( + { + "message": f"成功更新会话 {session_id} 的人格为 {persona_name}" + } + ) + .__dict__ + ) except Exception as e: error_msg = f"更新会话人格失败: {str(e)}\n{traceback.format_exc()}" logger.error(error_msg) return Response().error(f"更新会话人格失败: {str(e)}").__dict__ + async def _update_single_session_provider( + self, session_id: str, provider_id: str, provider_type_enum + ): + """更新单个会话的 provider 的内部方法""" + provider_manager = self.core_lifecycle.star_context.provider_manager + await provider_manager.set_provider( + provider_id=provider_id, + provider_type=provider_type_enum, + umo=session_id, + ) + async def update_session_provider(self): - """更新指定会话的 provider""" + """更新指定会话的 provider,支持批量操作""" try: data = await request.get_json() - session_id = data.get("session_id") + is_batch = data.get("is_batch", False) provider_id = data.get("provider_id") - # "chat_completion", "speech_to_text", "text_to_speech" provider_type = data.get("provider_type") - if not session_id or not provider_id or not provider_type: + if not provider_id or not provider_type: return ( Response() - .error("缺少必要参数: session_id, provider_id, provider_type") + .error("缺少必要参数: provider_id, provider_type") .__dict__ ) @@ -251,23 +324,35 @@ class SessionManagementRoute(Route): .__dict__ ) - # 设置 provider - provider_manager = self.core_lifecycle.star_context.provider_manager - await provider_manager.set_provider( - provider_id=provider_id, - provider_type=provider_type_enum, - umo=session_id, - ) + if is_batch: + session_ids = data.get("session_ids", []) + if not session_ids: + return Response().error("缺少必要参数: session_ids").__dict__ - return ( - Response() - .ok( - { - "message": f"成功更新会话 {session_id} 的 {provider_type} 提供商为 {provider_id}" - } + return await self._handle_batch_operation( + session_ids, + self._update_single_session_provider, + f"更新 {provider_type} 提供商", + provider_id=provider_id, + provider_type_enum=provider_type_enum, + ) + else: + session_id = data.get("session_id") + if not session_id: + return Response().error("缺少必要参数: session_id").__dict__ + + await self._update_single_session_provider( + session_id, provider_id, provider_type_enum + ) + return ( + Response() + .ok( + { + "message": f"成功更新会话 {session_id} 的 {provider_type} 提供商为 {provider_id}" + } + ) + .__dict__ ) - .__dict__ - ) except Exception as e: error_msg = f"更新会话提供商失败: {str(e)}\n{traceback.format_exc()}" @@ -376,66 +461,98 @@ class SessionManagementRoute(Route): logger.error(error_msg) return Response().error(f"更新会话插件状态失败: {str(e)}").__dict__ + async def _update_single_session_llm(self, session_id: str, enabled: bool): + """更新单个会话的LLM状态的内部方法""" + SessionServiceManager.set_llm_status_for_session(session_id, enabled) + async def update_session_llm(self): - """更新指定会话的LLM启停状态""" + """更新指定会话的LLM启停状态,支持批量操作""" try: data = await request.get_json() - session_id = data.get("session_id") + is_batch = data.get("is_batch", False) enabled = data.get("enabled") - if not session_id: - return Response().error("缺少必要参数: session_id").__dict__ - if enabled is None: return Response().error("缺少必要参数: enabled").__dict__ - # 使用 SessionServiceManager 更新LLM状态 - SessionServiceManager.set_llm_status_for_session(session_id, enabled) + if is_batch: + session_ids = data.get("session_ids", []) + if not session_ids: + return Response().error("缺少必要参数: session_ids").__dict__ - return ( - Response() - .ok( - { - "message": f"LLM已{'启用' if enabled else '禁用'}", - "session_id": session_id, - "llm_enabled": enabled, - } + result = await self._handle_batch_operation( + session_ids, + self._update_single_session_llm, + f"{'启用' if enabled else '禁用'}LLM", + enabled=enabled, + ) + return result + else: + session_id = data.get("session_id") + if not session_id: + return Response().error("缺少必要参数: session_id").__dict__ + + await self._update_single_session_llm(session_id, enabled) + return ( + Response() + .ok( + { + "message": f"LLM已{'启用' if enabled else '禁用'}", + "session_id": session_id, + "llm_enabled": enabled, + } + ) + .__dict__ ) - .__dict__ - ) except Exception as e: error_msg = f"更新会话LLM状态失败: {str(e)}\n{traceback.format_exc()}" logger.error(error_msg) return Response().error(f"更新会话LLM状态失败: {str(e)}").__dict__ + async def _update_single_session_tts(self, session_id: str, enabled: bool): + """更新单个会话的TTS状态的内部方法""" + SessionServiceManager.set_tts_status_for_session(session_id, enabled) + async def update_session_tts(self): - """更新指定会话的TTS启停状态""" + """更新指定会话的TTS启停状态,支持批量操作""" try: data = await request.get_json() - session_id = data.get("session_id") + is_batch = data.get("is_batch", False) enabled = data.get("enabled") - if not session_id: - return Response().error("缺少必要参数: session_id").__dict__ - if enabled is None: return Response().error("缺少必要参数: enabled").__dict__ - # 使用 SessionServiceManager 更新TTS状态 - SessionServiceManager.set_tts_status_for_session(session_id, enabled) + if is_batch: + session_ids = data.get("session_ids", []) + if not session_ids: + return Response().error("缺少必要参数: session_ids").__dict__ - return ( - Response() - .ok( - { - "message": f"TTS已{'启用' if enabled else '禁用'}", - "session_id": session_id, - "tts_enabled": enabled, - } + result = await self._handle_batch_operation( + session_ids, + self._update_single_session_tts, + f"{'启用' if enabled else '禁用'}TTS", + enabled=enabled, + ) + return result + else: + session_id = data.get("session_id") + if not session_id: + return Response().error("缺少必要参数: session_id").__dict__ + + await self._update_single_session_tts(session_id, enabled) + return ( + Response() + .ok( + { + "message": f"TTS已{'启用' if enabled else '禁用'}", + "session_id": session_id, + "tts_enabled": enabled, + } + ) + .__dict__ ) - .__dict__ - ) except Exception as e: error_msg = f"更新会话TTS状态失败: {str(e)}\n{traceback.format_exc()}" @@ -507,3 +624,43 @@ class SessionManagementRoute(Route): error_msg = f"更新会话整体状态失败: {str(e)}\n{traceback.format_exc()}" logger.error(error_msg) return Response().error(f"更新会话整体状态失败: {str(e)}").__dict__ + + async def delete_session(self): + """删除指定会话及其所有相关数据""" + try: + data = await request.get_json() + session_id = data.get("session_id") + + if not session_id: + return Response().error("缺少必要参数: session_id").__dict__ + + # 删除会话的所有相关数据 + conversation_manager = self.core_lifecycle.conversation_manager + + # 1. 删除会话的所有对话 + try: + await conversation_manager.delete_conversations_by_user_id(session_id) + except Exception as e: + logger.warning(f"删除会话 {session_id} 的对话失败: {str(e)}") + + # 2. 清除会话的偏好设置数据(清空该会话的所有配置) + try: + await sp.clear_async("umo", session_id) + except Exception as e: + logger.warning(f"清除会话 {session_id} 的偏好设置失败: {str(e)}") + + return ( + Response() + .ok( + { + "message": f"会话 {session_id} 及其相关所有对话数据已成功删除", + "session_id": session_id, + } + ) + .__dict__ + ) + + except Exception as e: + error_msg = f"删除会话失败: {str(e)}\n{traceback.format_exc()}" + logger.error(error_msg) + return Response().error(f"删除会话失败: {str(e)}").__dict__ diff --git a/dashboard/src/i18n/locales/en-US/features/session-management.json b/dashboard/src/i18n/locales/en-US/features/session-management.json index bc1ed90a4..84ddc0cdb 100644 --- a/dashboard/src/i18n/locales/en-US/features/session-management.json +++ b/dashboard/src/i18n/locales/en-US/features/session-management.json @@ -7,7 +7,8 @@ "apply": "Apply Batch Settings", "editName": "Edit Session Name", "save": "Save", - "cancel": "Cancel" + "cancel": "Cancel", + "delete": "Delete" }, "sessions": { "activeSessions": "Active Sessions", @@ -29,7 +30,8 @@ "ttsProvider": "TTS Provider", "llmStatus": "LLM Status", "ttsStatus": "TTS Status", - "pluginManagement": "Plugin Management" + "pluginManagement": "Plugin Management", + "actions": "Actions" } }, "status": { @@ -65,6 +67,10 @@ "fullSessionId": "Full Session ID", "hint": "Custom names help you easily identify sessions. The small information icon (!) will show the actual UMO when hovering." }, + "deleteConfirm": { + "message": "Are you sure you want to delete session {sessionName}?", + "warning": "This action will permanently delete all chat history and preference settings for this session (except for data linked via plugins), and this cannot be undone. Continue?" + }, "messages": { "refreshSuccess": "Session list refreshed", "personaUpdateSuccess": "Persona updated successfully", @@ -82,6 +88,8 @@ "pluginStatusSuccess": "Plugin {name} {status}", "pluginStatusError": "Failed to update plugin status", "nameUpdateSuccess": "Session name updated successfully", - "nameUpdateError": "Failed to update session name" + "nameUpdateError": "Failed to update session name", + "deleteSuccess": "Session deleted successfully", + "deleteError": "Failed to delete session" } } diff --git a/dashboard/src/i18n/locales/zh-CN/features/session-management.json b/dashboard/src/i18n/locales/zh-CN/features/session-management.json index c3364c5b0..6e93ef76f 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/session-management.json +++ b/dashboard/src/i18n/locales/zh-CN/features/session-management.json @@ -7,7 +7,8 @@ "apply": "应用批量设置", "editName": "备注", "save": "保存", - "cancel": "取消" + "cancel": "取消", + "delete": "删除" }, "sessions": { "activeSessions": "活跃会话", @@ -29,7 +30,8 @@ "ttsProvider": "语音合成模型", "llmStatus": "启用 LLM", "ttsStatus": "启用 TTS", - "pluginManagement": "插件管理" + "pluginManagement": "插件管理", + "actions": "操作" } }, "status": { @@ -65,6 +67,10 @@ "fullSessionId": "完整会话ID", "hint": "自定义名称帮助您轻松识别会话。当设置了自定义名称时,会显示一个小感叹号标识(!),鼠标悬停时会显示实际的UMO。" }, + "deleteConfirm": { + "message": "确定要删除会话 {sessionName} 吗?", + "warning": "此操作将永久删除本次会话的「全部对话记录」与「偏好设置」(插件对会话的关联数据除外),且无法恢复。确认继续?" + }, "messages": { "refreshSuccess": "会话列表已刷新", "personaUpdateSuccess": "人格更新成功", @@ -82,6 +88,8 @@ "pluginStatusSuccess": "插件 {name} {status}", "pluginStatusError": "插件状态更新失败", "nameUpdateSuccess": "会话名称更新成功", - "nameUpdateError": "会话名称更新失败" + "nameUpdateError": "会话名称更新失败", + "deleteSuccess": "会话删除成功", + "deleteError": "会话删除失败" } } diff --git a/dashboard/src/views/ConversationPage.vue b/dashboard/src/views/ConversationPage.vue index fc7cc0d43..35242db78 100644 --- a/dashboard/src/views/ConversationPage.vue +++ b/dashboard/src/views/ConversationPage.vue @@ -601,19 +601,15 @@ export default { } if (this.search) { - params.search = this.search; + params.search = this.search.trim(); } // 添加排除条件 params.exclude_ids = 'astrbot'; params.exclude_platforms = 'webchat'; - console.log(`正在请求对话列表: /api/conversation/list 参数:`, params); - const response = await axios.get('/api/conversation/list', { params }); - console.log('收到对话列表响应:', response.data); - this.lastAppliedFilters = { ...this.currentFilters }; // 记录已应用的筛选条件 if (response.data.status === "ok") { diff --git a/dashboard/src/views/SessionManagementPage.vue b/dashboard/src/views/SessionManagementPage.vue index a769c1acf..1704dfab9 100644 --- a/dashboard/src/views/SessionManagementPage.vue +++ b/dashboard/src/views/SessionManagementPage.vue @@ -141,6 +141,17 @@ + + +