From 2f61df1cc5f6e42f91dc4f7f8b2b72058a0d69d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Mon, 1 Dec 2025 23:30:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BE=A4=20ID=20=E5=A4=84=E7=90=86=E5=92=8C?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../message_receive/uni_message_sender.py | 23 +- src/webui/chat_routes.py | 441 ++++++++++++++++-- 2 files changed, 414 insertions(+), 50 deletions(-) diff --git a/src/chat/message_receive/uni_message_sender.py b/src/chat/message_receive/uni_message_sender.py index 13ff3641..93f5a0fa 100644 --- a/src/chat/message_receive/uni_message_sender.py +++ b/src/chat/message_receive/uni_message_sender.py @@ -18,6 +18,9 @@ logger = get_logger("sender") # WebUI 聊天室的消息广播器(延迟导入避免循环依赖) _webui_chat_broadcaster = None +# 虚拟群 ID 前缀(与 chat_routes.py 保持一致) +VIRTUAL_GROUP_ID_PREFIX = "webui_virtual_group_" + def get_webui_chat_broadcaster(): """获取 WebUI 聊天室广播器""" @@ -32,16 +35,24 @@ def get_webui_chat_broadcaster(): return _webui_chat_broadcaster +def is_webui_virtual_group(group_id: str) -> bool: + """检查是否是 WebUI 虚拟群""" + return group_id and group_id.startswith(VIRTUAL_GROUP_ID_PREFIX) + + async def _send_message(message: MessageSending, show_log=True) -> bool: """合并后的消息发送函数,包含WS发送和日志记录""" message_preview = truncate_message(message.processed_plain_text, max_length=200) platform = message.message_info.platform + group_id = message.message_info.group_info.group_id if message.message_info.group_info else None try: - # 检查是否是 WebUI 平台的消息 + # 检查是否是 WebUI 平台的消息,或者是 WebUI 虚拟群的消息 chat_manager, webui_platform = get_webui_chat_broadcaster() - if platform == webui_platform and chat_manager is not None: - # WebUI 聊天室消息,通过 WebSocket 广播 + is_webui_message = (platform == webui_platform) or is_webui_virtual_group(group_id) + + if is_webui_message and chat_manager is not None: + # WebUI 聊天室消息(包括虚拟身份模式),通过 WebSocket 广播 import time from src.config.config import global_config @@ -51,6 +62,7 @@ async def _send_message(message: MessageSending, show_log=True) -> bool: "content": message.processed_plain_text, "message_type": "text", "timestamp": time.time(), + "group_id": group_id, # 包含群 ID 以便前端区分不同的聊天标签 "sender": { "name": global_config.bot.nickname, "avatar": None, @@ -63,7 +75,10 @@ async def _send_message(message: MessageSending, show_log=True) -> bool: # 无需手动保存 if show_log: - logger.info(f"已将消息 '{message_preview}' 发往 WebUI 聊天室") + if is_webui_virtual_group(group_id): + logger.info(f"已将消息 '{message_preview}' 发往 WebUI 虚拟群 (平台: {platform})") + else: + logger.info(f"已将消息 '{message_preview}' 发往 WebUI 聊天室") return True # 直接调用API发送消息 diff --git a/src/webui/chat_routes.py b/src/webui/chat_routes.py index f0403d09..14d8d9d2 100644 --- a/src/webui/chat_routes.py +++ b/src/webui/chat_routes.py @@ -1,4 +1,9 @@ -"""本地聊天室路由 - WebUI 与麦麦直接对话""" +"""本地聊天室路由 - WebUI 与麦麦直接对话 + +支持两种模式: +1. WebUI 模式:使用 WebUI 平台独立身份聊天 +2. 虚拟身份模式:使用真实平台用户的身份,在虚拟群聊中与麦麦对话 +""" import time import uuid @@ -7,7 +12,7 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query from pydantic import BaseModel from src.common.logger import get_logger -from src.common.database.database_model import Messages +from src.common.database.database_model import Messages, PersonInfo from src.config.config import global_config from src.chat.message_receive.bot import chat_bot @@ -19,10 +24,25 @@ router = APIRouter(prefix="/api/chat", tags=["LocalChat"]) WEBUI_CHAT_GROUP_ID = "webui_local_chat" WEBUI_CHAT_PLATFORM = "webui" +# 虚拟身份模式的群 ID 前缀 +VIRTUAL_GROUP_ID_PREFIX = "webui_virtual_group_" + # 固定的 WebUI 用户 ID 前缀 WEBUI_USER_ID_PREFIX = "webui_user_" +class VirtualIdentityConfig(BaseModel): + """虚拟身份配置""" + + enabled: bool = False # 是否启用虚拟身份模式 + platform: Optional[str] = None # 目标平台(如 qq, discord 等) + person_id: Optional[str] = None # PersonInfo 的 person_id + user_id: Optional[str] = None # 原始平台用户 ID + user_nickname: Optional[str] = None # 用户昵称 + group_id: Optional[str] = None # 虚拟群 ID(自动生成或用户指定) + group_name: Optional[str] = None # 虚拟群名(用户自定义) + + class ChatHistoryMessage(BaseModel): """聊天历史消息""" @@ -41,12 +61,25 @@ class ChatHistoryManager: def __init__(self, max_messages: int = 200): self.max_messages = max_messages - def _message_to_dict(self, msg: Messages) -> Dict[str, Any]: - """将数据库消息转换为前端格式""" + def _message_to_dict(self, msg: Messages, group_id: Optional[str] = None) -> Dict[str, Any]: + """将数据库消息转换为前端格式 + + Args: + msg: 数据库消息对象 + group_id: 群 ID,用于判断是否是虚拟群 + """ # 判断是否是机器人消息 - # WebUI 用户的 user_id 以 "webui_" 开头,其他都是机器人消息 user_id = msg.user_id or "" - is_bot = not user_id.startswith("webui_") and not user_id.startswith(WEBUI_USER_ID_PREFIX) + + # 对于虚拟群,通过比较机器人 QQ 账号来判断 + # 对于普通 WebUI 群,检查 user_id 是否以 webui_ 开头 + if group_id and group_id.startswith(VIRTUAL_GROUP_ID_PREFIX): + # 虚拟群:user_id 等于机器人 QQ 账号的是机器人消息 + bot_qq = str(global_config.bot.qq_account) + is_bot = user_id == bot_qq + else: + # 普通 WebUI 群:不以 webui_ 开头的是机器人消息 + is_bot = not user_id.startswith("webui_") and not user_id.startswith(WEBUI_USER_ID_PREFIX) return { "id": msg.message_id, @@ -58,32 +91,44 @@ class ChatHistoryManager: "is_bot": is_bot, } - def get_history(self, limit: int = 50) -> List[Dict[str, Any]]: - """从数据库获取最近的历史记录""" + def get_history(self, limit: int = 50, group_id: Optional[str] = None) -> List[Dict[str, Any]]: + """从数据库获取最近的历史记录 + + Args: + limit: 获取的消息数量 + group_id: 群 ID,默认为 WEBUI_CHAT_GROUP_ID + """ + target_group_id = group_id if group_id else WEBUI_CHAT_GROUP_ID try: - # 查询 WebUI 平台的消息,按时间排序 + # 查询指定群的消息,按时间排序 messages = ( Messages.select() - .where(Messages.chat_info_group_id == WEBUI_CHAT_GROUP_ID) + .where(Messages.chat_info_group_id == target_group_id) .order_by(Messages.time.desc()) .limit(limit) ) # 转换为列表并反转(使最旧的消息在前) - result = [self._message_to_dict(msg) for msg in messages] + # 传递 group_id 以便正确判断虚拟群中的机器人消息 + result = [self._message_to_dict(msg, target_group_id) for msg in messages] result.reverse() - logger.debug(f"从数据库加载了 {len(result)} 条聊天记录") + logger.debug(f"从数据库加载了 {len(result)} 条聊天记录 (group_id={target_group_id})") return result except Exception as e: logger.error(f"从数据库加载聊天记录失败: {e}") return [] - def clear_history(self) -> int: - """清空 WebUI 聊天历史记录""" + def clear_history(self, group_id: Optional[str] = None) -> int: + """清空聊天历史记录 + + Args: + group_id: 群 ID,默认清空 WebUI 默认聊天室 + """ + target_group_id = group_id if group_id else WEBUI_CHAT_GROUP_ID try: - deleted = Messages.delete().where(Messages.chat_info_group_id == WEBUI_CHAT_GROUP_ID).execute() - logger.info(f"已清空 {deleted} 条 WebUI 聊天记录") + deleted = Messages.delete().where(Messages.chat_info_group_id == target_group_id).execute() + logger.info(f"已清空 {deleted} 条聊天记录 (group_id={target_group_id})") return deleted except Exception as e: logger.error(f"清空聊天记录失败: {e}") @@ -132,27 +177,57 @@ chat_manager = ChatConnectionManager() def create_message_data( - content: str, user_id: str, user_name: str, message_id: Optional[str] = None, is_at_bot: bool = True + content: str, + user_id: str, + user_name: str, + message_id: Optional[str] = None, + is_at_bot: bool = True, + virtual_config: Optional[VirtualIdentityConfig] = None, ) -> Dict[str, Any]: - """创建符合麦麦消息格式的消息数据""" + """创建符合麦麦消息格式的消息数据 + + Args: + content: 消息内容 + user_id: 用户 ID + user_name: 用户昵称 + message_id: 消息 ID(可选,自动生成) + is_at_bot: 是否 @ 机器人 + virtual_config: 虚拟身份配置(可选,启用后使用真实平台身份) + """ if message_id is None: message_id = str(uuid.uuid4()) + # 确定使用的平台、群信息和用户信息 + if virtual_config and virtual_config.enabled: + # 虚拟身份模式:使用真实平台身份 + platform = virtual_config.platform or WEBUI_CHAT_PLATFORM + group_id = virtual_config.group_id or f"{VIRTUAL_GROUP_ID_PREFIX}{uuid.uuid4().hex[:8]}" + group_name = virtual_config.group_name or "WebUI虚拟群聊" + actual_user_id = virtual_config.user_id or user_id + actual_user_name = virtual_config.user_nickname or user_name + else: + # 标准 WebUI 模式 + platform = WEBUI_CHAT_PLATFORM + group_id = WEBUI_CHAT_GROUP_ID + group_name = "WebUI本地聊天室" + actual_user_id = user_id + actual_user_name = user_name + return { "message_info": { - "platform": WEBUI_CHAT_PLATFORM, + "platform": platform, "message_id": message_id, "time": time.time(), "group_info": { - "group_id": WEBUI_CHAT_GROUP_ID, - "group_name": "WebUI本地聊天室", - "platform": WEBUI_CHAT_PLATFORM, + "group_id": group_id, + "group_name": group_name, + "platform": platform, }, "user_info": { - "user_id": user_id, - "user_nickname": user_name, - "user_cardname": user_name, - "platform": WEBUI_CHAT_PLATFORM, + "user_id": actual_user_id, + "user_nickname": actual_user_name, + "user_cardname": actual_user_name, + "platform": platform, }, "additional_config": { "at_bot": is_at_bot, @@ -180,12 +255,15 @@ def create_message_data( async def get_chat_history( limit: int = Query(default=50, ge=1, le=200), user_id: Optional[str] = Query(default=None), # 保留参数兼容性,但不用于过滤 + group_id: Optional[str] = Query(default=None), # 可选:指定群 ID 获取历史 ): """获取聊天历史记录 所有 WebUI 用户共享同一个聊天室,因此返回所有历史记录 + 如果指定了 group_id,则获取该虚拟群的历史记录 """ - history = chat_history.get_history(limit) + target_group_id = group_id if group_id else WEBUI_CHAT_GROUP_ID + history = chat_history.get_history(limit, target_group_id) return { "success": True, "messages": history, @@ -193,10 +271,92 @@ async def get_chat_history( } +@router.get("/platforms") +async def get_available_platforms(): + """获取可用平台列表 + + 从 PersonInfo 表中获取所有已知的平台 + """ + try: + from peewee import fn + + # 查询所有不同的平台 + platforms = ( + PersonInfo.select(PersonInfo.platform, fn.COUNT(PersonInfo.id).alias("count")) + .group_by(PersonInfo.platform) + .order_by(fn.COUNT(PersonInfo.id).desc()) + ) + + result = [] + for p in platforms: + if p.platform: # 排除空平台 + result.append({"platform": p.platform, "count": p.count}) + + return {"success": True, "platforms": result} + except Exception as e: + logger.error(f"获取平台列表失败: {e}") + return {"success": False, "error": str(e), "platforms": []} + + +@router.get("/persons") +async def get_persons_by_platform( + platform: str = Query(..., description="平台名称"), + search: Optional[str] = Query(default=None, description="搜索关键词"), + limit: int = Query(default=50, ge=1, le=200), +): + """获取指定平台的用户列表 + + Args: + platform: 平台名称(如 qq, discord 等) + search: 搜索关键词(匹配昵称、用户名、user_id) + limit: 返回数量限制 + """ + try: + # 构建查询 + query = PersonInfo.select().where(PersonInfo.platform == platform) + + # 搜索过滤 + if search: + query = query.where( + (PersonInfo.person_name.contains(search)) + | (PersonInfo.nickname.contains(search)) + | (PersonInfo.user_id.contains(search)) + ) + + # 按最后交互时间排序,优先显示活跃用户 + from peewee import Case + + query = query.order_by(Case(None, [(PersonInfo.last_know.is_null(), 1)], 0), PersonInfo.last_know.desc()) + query = query.limit(limit) + + result = [] + for person in query: + result.append( + { + "person_id": person.person_id, + "user_id": person.user_id, + "person_name": person.person_name, + "nickname": person.nickname, + "is_known": person.is_known, + "platform": person.platform, + "display_name": person.person_name or person.nickname or person.user_id, + } + ) + + return {"success": True, "persons": result, "total": len(result)} + except Exception as e: + logger.error(f"获取用户列表失败: {e}") + return {"success": False, "error": str(e), "persons": []} + + @router.delete("/history") -async def clear_chat_history(): - """清空聊天历史记录""" - deleted = chat_history.clear_history() +async def clear_chat_history(group_id: Optional[str] = Query(default=None)): + """清空聊天历史记录 + + Args: + group_id: 可选,指定要清空的群 ID,默认清空 WebUI 默认聊天室 + """ + deleted = chat_history.clear_history(group_id) return { "success": True, "message": f"已清空 {deleted} 条聊天记录", @@ -208,12 +368,22 @@ async def websocket_chat( websocket: WebSocket, user_id: Optional[str] = Query(default=None), user_name: Optional[str] = Query(default="WebUI用户"), + platform: Optional[str] = Query(default=None), + person_id: Optional[str] = Query(default=None), + group_name: Optional[str] = Query(default=None), + group_id: Optional[str] = Query(default=None), # 前端传递的稳定 group_id ): """WebSocket 聊天端点 Args: user_id: 用户唯一标识(由前端生成并持久化) user_name: 用户显示昵称(可修改) + platform: 虚拟身份模式的平台(可选) + person_id: 虚拟身份模式的用户 person_id(可选) + group_name: 虚拟身份模式的群名(可选) + group_id: 虚拟身份模式的群 ID(可选,由前端生成并持久化) + + 虚拟身份模式可通过 URL 参数直接配置,或通过消息中的 set_virtual_identity 配置 """ # 生成会话 ID(每次连接都是新的) session_id = str(uuid.uuid4()) @@ -225,23 +395,60 @@ async def websocket_chat( # 确保 user_id 有正确的前缀 user_id = f"{WEBUI_USER_ID_PREFIX}{user_id}" + # 当前会话的虚拟身份配置(可通过消息动态更新) + current_virtual_config: Optional[VirtualIdentityConfig] = None + + # 如果 URL 参数中提供了虚拟身份信息,自动配置 + if platform and person_id: + try: + person = PersonInfo.get_or_none(PersonInfo.person_id == person_id) + if person: + # 使用前端传递的 group_id,如果没有则生成一个稳定的 + virtual_group_id = group_id or f"{VIRTUAL_GROUP_ID_PREFIX}{platform}_{person.user_id}" + current_virtual_config = VirtualIdentityConfig( + enabled=True, + platform=person.platform, + person_id=person.person_id, + user_id=person.user_id, + user_nickname=person.person_name or person.nickname or person.user_id, + group_id=virtual_group_id, + group_name=group_name or "WebUI虚拟群聊", + ) + logger.info(f"虚拟身份模式已通过 URL 参数激活: {current_virtual_config.user_nickname} @ {current_virtual_config.platform}, group_id={virtual_group_id}") + except Exception as e: + logger.warning(f"通过 URL 参数配置虚拟身份失败: {e}") + await chat_manager.connect(websocket, session_id, user_id) try: - # 发送会话信息(包含用户 ID,前端需要保存) - await chat_manager.send_message( - session_id, - { - "type": "session_info", - "session_id": session_id, - "user_id": user_id, - "user_name": user_name, - "bot_name": global_config.bot.nickname, - }, - ) + # 构建会话信息 + session_info_data = { + "type": "session_info", + "session_id": session_id, + "user_id": user_id, + "user_name": user_name, + "bot_name": global_config.bot.nickname, + } - # 发送历史记录 - history = chat_history.get_history(50) + # 如果有虚拟身份配置,添加到会话信息中 + if current_virtual_config and current_virtual_config.enabled: + session_info_data["virtual_mode"] = True + session_info_data["group_id"] = current_virtual_config.group_id + session_info_data["virtual_identity"] = { + "platform": current_virtual_config.platform, + "user_id": current_virtual_config.user_id, + "user_nickname": current_virtual_config.user_nickname, + "group_name": current_virtual_config.group_name, + } + + # 发送会话信息(包含用户 ID,前端需要保存) + await chat_manager.send_message(session_id, session_info_data) + + # 发送历史记录(根据模式选择不同的群) + if current_virtual_config and current_virtual_config.enabled: + history = chat_history.get_history(50, current_virtual_config.group_id) + else: + history = chat_history.get_history(50) if history: await chat_manager.send_message( session_id, @@ -252,11 +459,16 @@ async def websocket_chat( ) # 发送欢迎消息(不保存到历史) + if current_virtual_config and current_virtual_config.enabled: + welcome_msg = f"已以 {current_virtual_config.user_nickname} 的身份连接到「{current_virtual_config.group_name}」,开始与 {global_config.bot.nickname} 对话吧!" + else: + welcome_msg = f"已连接到本地聊天室,可以开始与 {global_config.bot.nickname} 对话了!" + await chat_manager.send_message( session_id, { "type": "system", - "content": f"已连接到本地聊天室,可以开始与 {global_config.bot.nickname} 对话了!", + "content": welcome_msg, "timestamp": time.time(), }, ) @@ -275,6 +487,14 @@ async def websocket_chat( message_id = str(uuid.uuid4()) timestamp = time.time() + # 确定发送者信息(根据是否使用虚拟身份) + if current_virtual_config and current_virtual_config.enabled: + sender_name = current_virtual_config.user_nickname or current_user_name + sender_user_id = current_virtual_config.user_id or user_id + else: + sender_name = current_user_name + sender_user_id = user_id + # 广播用户消息给所有连接(包括发送者) # 注意:用户消息会在 chat_bot.message_process 中自动保存到数据库 await chat_manager.broadcast( @@ -284,10 +504,11 @@ async def websocket_chat( "message_id": message_id, "timestamp": timestamp, "sender": { - "name": current_user_name, - "user_id": user_id, + "name": sender_name, + "user_id": sender_user_id, "is_bot": False, }, + "virtual_mode": current_virtual_config.enabled if current_virtual_config else False, } ) @@ -298,6 +519,7 @@ async def websocket_chat( user_name=current_user_name, message_id=message_id, is_at_bot=True, + virtual_config=current_virtual_config, ) try: @@ -352,6 +574,133 @@ async def websocket_chat( }, ) + elif data.get("type") == "set_virtual_identity": + # 设置或更新虚拟身份配置 + virtual_data = data.get("config", {}) + if virtual_data.get("enabled"): + # 验证必要字段 + if not virtual_data.get("platform") or not virtual_data.get("person_id"): + await chat_manager.send_message( + session_id, + { + "type": "error", + "content": "虚拟身份配置缺少必要字段: platform 和 person_id", + "timestamp": time.time(), + }, + ) + continue + + # 获取用户信息 + try: + person = PersonInfo.get_or_none(PersonInfo.person_id == virtual_data.get("person_id")) + if not person: + await chat_manager.send_message( + session_id, + { + "type": "error", + "content": f"找不到用户: {virtual_data.get('person_id')}", + "timestamp": time.time(), + }, + ) + continue + + # 生成虚拟群 ID + custom_group_id = virtual_data.get("group_id") + if custom_group_id: + group_id = f"{VIRTUAL_GROUP_ID_PREFIX}{custom_group_id}" + else: + group_id = f"{VIRTUAL_GROUP_ID_PREFIX}{session_id[:8]}" + + current_virtual_config = VirtualIdentityConfig( + enabled=True, + platform=person.platform, + person_id=person.person_id, + user_id=person.user_id, + user_nickname=person.person_name or person.nickname or person.user_id, + group_id=group_id, + group_name=virtual_data.get("group_name", "WebUI虚拟群聊"), + ) + + # 发送虚拟身份已激活的消息 + await chat_manager.send_message( + session_id, + { + "type": "virtual_identity_set", + "config": { + "enabled": True, + "platform": current_virtual_config.platform, + "user_id": current_virtual_config.user_id, + "user_nickname": current_virtual_config.user_nickname, + "group_id": current_virtual_config.group_id, + "group_name": current_virtual_config.group_name, + }, + "timestamp": time.time(), + }, + ) + + # 加载虚拟群的历史记录 + virtual_history = chat_history.get_history(50, current_virtual_config.group_id) + await chat_manager.send_message( + session_id, + { + "type": "history", + "messages": virtual_history, + "group_id": current_virtual_config.group_id, + }, + ) + + # 发送系统消息 + await chat_manager.send_message( + session_id, + { + "type": "system", + "content": f"已切换到虚拟身份模式:以 {current_virtual_config.user_nickname} 的身份在「{current_virtual_config.group_name}」与 {global_config.bot.nickname} 对话", + "timestamp": time.time(), + }, + ) + + except Exception as e: + logger.error(f"设置虚拟身份失败: {e}") + await chat_manager.send_message( + session_id, + { + "type": "error", + "content": f"设置虚拟身份失败: {str(e)}", + "timestamp": time.time(), + }, + ) + else: + # 禁用虚拟身份模式 + current_virtual_config = None + await chat_manager.send_message( + session_id, + { + "type": "virtual_identity_set", + "config": {"enabled": False}, + "timestamp": time.time(), + }, + ) + + # 重新加载默认聊天室历史 + default_history = chat_history.get_history(50, WEBUI_CHAT_GROUP_ID) + await chat_manager.send_message( + session_id, + { + "type": "history", + "messages": default_history, + "group_id": WEBUI_CHAT_GROUP_ID, + }, + ) + + await chat_manager.send_message( + session_id, + { + "type": "system", + "content": "已切换回 WebUI 独立用户模式", + "timestamp": time.time(), + }, + ) + except WebSocketDisconnect: logger.info(f"WebSocket 断开: session={session_id}, user={user_id}") except Exception as e: