diff --git a/adapters/router.py b/adapters/router.py index e1c97ef..372540e 100644 --- a/adapters/router.py +++ b/adapters/router.py @@ -356,7 +356,8 @@ class DiscordToOneBotConverter: # 注入 Discord 特定信息(用于跨平台插件识别) discord_channel_id = discord_message.channel.id if not isinstance(discord_message.channel, discord.DMChannel) else None - discord_username = discord_message.author.name + # 使用 global_name (显示名称/昵称) 如果存在,否则使用 name (用户名) + discord_username = getattr(discord_message.author, 'global_name', None) or discord_message.author.name discord_discriminator = f"#{discord_message.author.discriminator}" if discord_message.author.discriminator != "0" else "" if is_private: diff --git a/core/managers/__init__.py b/core/managers/__init__.py new file mode 100644 index 0000000..4e88f1a --- /dev/null +++ b/core/managers/__init__.py @@ -0,0 +1,60 @@ +""" +管理器包 + +这个包集中了机器人核心的单例管理器。 +通过从这里导入,可以确保在整个应用中访问到的都是同一个实例。 +""" +from .command_manager import matcher as command_manager +from .permission_manager import PermissionManager +from .plugin_manager import PluginManager +from .redis_manager import RedisManager +from .mysql_manager import MySQLManager +from .browser_manager import BrowserManager +from .image_manager import ImageManager +from .reverse_ws_manager import ReverseWSManager +from .thread_manager import thread_manager +from .vectordb_manager import vectordb_manager + +# --- 实例化所有单例管理器 --- + +# 权限管理器(包含了管理员管理功能) +permission_manager = PermissionManager() + +# 命令与事件管理器 (别名 matcher) +matcher = command_manager + +# 插件管理器 +plugin_manager = PluginManager(command_manager) +# plugin_manager.load_all_plugins() + +# Redis 管理器 +redis_manager = RedisManager() + +# MySQL 管理器 +mysql_manager = MySQLManager() + +# 浏览器管理器 +browser_manager = BrowserManager() + +# 图片管理器 +image_manager = ImageManager() + +# 反向 WebSocket 管理器 +reverse_ws_manager = ReverseWSManager() + +# 线程管理器 +thread_manager.start() + +__all__ = [ + "permission_manager", + "command_manager", + "matcher", + "plugin_manager", + "redis_manager", + "mysql_manager", + "browser_manager", + "image_manager", + "reverse_ws_manager", + "thread_manager", + "vectordb_manager", +] diff --git a/data/vectordb/chroma.sqlite3 b/data/vectordb/chroma.sqlite3 new file mode 100644 index 0000000..c0ab1dd Binary files /dev/null and b/data/vectordb/chroma.sqlite3 differ diff --git a/plugins/ai_chat.py b/plugins/ai_chat.py new file mode 100644 index 0000000..1e94bcf --- /dev/null +++ b/plugins/ai_chat.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +""" +AI 聊天插件,支持向量数据库记忆功能 +""" +import time +import uuid +from core.managers.command_manager import matcher +from models.events.message import GroupMessageEvent, PrivateMessageEvent +from core.managers.vectordb_manager import vectordb_manager +from core.utils.logger import ModuleLogger +from core.config_loader import global_config + +logger = ModuleLogger("AIChat") + +# 尝试导入 OpenAI 客户端 +try: + from openai import AsyncOpenAI + OPENAI_AVAILABLE = True +except ImportError: + OPENAI_AVAILABLE = False + +async def get_ai_response(user_id: int, group_id: int, user_message: str) -> str: + """获取 AI 回复,包含向量数据库记忆""" + if not OPENAI_AVAILABLE: + return "请先安装 openai 库: pip install openai" + + # 从配置中获取 DeepSeek API 配置(复用跨平台插件的配置或全局配置) + api_key = getattr(global_config.cross_platform, 'deepseek_api_key', None) or "your-api-key" + api_url = getattr(global_config.cross_platform, 'deepseek_api_url', "https://api.deepseek.com/v1") + model = getattr(global_config.cross_platform, 'deepseek_model', "deepseek-chat") + + if api_key == "your-api-key": + return "请先在配置中设置 DeepSeek API Key" + + # 1. 从向量数据库检索相关记忆 + collection_name = f"chat_memory_{user_id}" + memory_context = "" + + try: + results = vectordb_manager.query_texts( + collection_name=collection_name, + query_texts=[user_message], + n_results=3 + ) + + if results and results.get("documents") and results["documents"][0]: + memory_context = "\n\n相关历史记忆:\n" + for i, doc in enumerate(results["documents"][0], 1): + memory_context += f"{i}. {doc}\n" + except Exception as e: + logger.error(f"检索聊天记忆失败: {e}") + + # 2. 构建 Prompt + system_prompt = f"""你是一个友好的 AI 助手。请根据用户的输入进行回复。 +如果提供了相关历史记忆,请参考这些记忆来保持对话的连贯性。{memory_context}""" + + try: + client = AsyncOpenAI( + api_key=api_key, + base_url=api_url.replace("/chat/completions", "") + ) + + response = await client.chat.completions.create( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message} + ], + temperature=0.7, + max_tokens=1000 + ) + + ai_reply = response.choices[0].message.content + + # 3. 将本次对话存入向量数据库 + if ai_reply: + try: + doc_id = str(uuid.uuid4()) + text_to_embed = f"用户: {user_message}\nAI: {ai_reply}" + metadata = { + "user_id": user_id, + "group_id": group_id, + "timestamp": int(time.time()) + } + + vectordb_manager.add_texts( + collection_name=collection_name, + texts=[text_to_embed], + metadatas=[metadata], + ids=[doc_id] + ) + except Exception as e: + logger.error(f"保存聊天记忆失败: {e}") + + return ai_reply + except Exception as e: + logger.error(f"AI 聊天请求失败: {e}") + return f"请求失败: {str(e)}" + +@matcher.command("chat", "聊天") +async def chat_command(event: GroupMessageEvent | PrivateMessageEvent, args: list[str]): + """AI 聊天命令""" + if not args: + await event.reply("请提供要聊天的内容,例如:/chat 你好") + return + + user_message = " ".join(args) + user_id = event.user_id + group_id = getattr(event, 'group_id', 0) + + await event.reply("正在思考中...") + reply = await get_ai_response(user_id, group_id, user_message) + await event.reply(reply) diff --git a/plugins/discord-cross/handlers.py b/plugins/discord-cross/handlers.py index 1e13b51..bc92f9a 100644 --- a/plugins/discord-cross/handlers.py +++ b/plugins/discord-cross/handlers.py @@ -148,7 +148,7 @@ async def handle_qq_group_message(event: GroupMessageEvent): group_name = f"群{group_id}" await handle_qq_message( - nickname=event.sender.nickname or event.sender.card or str(event.user_id), + nickname=event.sender.card or event.sender.nickname or str(event.user_id), user_id=event.user_id, group_name=group_name, group_id=group_id, diff --git a/src/neobot/adapters/discord_adapter.py b/src/neobot/adapters/discord_adapter.py index 6e8b33a..3809872 100644 --- a/src/neobot/adapters/discord_adapter.py +++ b/src/neobot/adapters/discord_adapter.py @@ -112,7 +112,8 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object): try: data = json.loads(message["data"]) if data.get("type") == "send_message": - await self.handle_send_message(data) + # 使用 asyncio.create_task 异步处理消息,避免阻塞订阅循环 + asyncio.create_task(self.handle_send_message(data)) except json.JSONDecodeError as e: self.logger.error(f"[DiscordAdapter] 解析 Redis 消息失败: {e}") except Exception as e: