From 8fc4cd4c3b5cd3f8ab5217c9264197e9b7d0dbd4 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Wed, 26 Nov 2025 16:40:31 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E8=BF=87=E6=97=B6=E7=9A=84napcat=5Fadapter=5Fplugin=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从napcat_adapter_plugin中删除了stream_router.py、utils.py、video_handler.py、websocket_manager.py和todo.md文件。 - 在napcat_cache.json中为组和成员信息引入了一种新的缓存结构。 - 通过移除未使用的模块和整合功能,简化了插件的架构。 --- .../interest_system/bot_interest_manager.py | 59 +- src/chat/message_receive/message_handler.py | 4 + src/plugin_system/__init__.py | 1 + src/plugin_system/base/base_adapter.py | 2 +- src/plugin_system/base/base_plugin.py | 42 +- src/plugin_system/base/component_types.py | 1 + src/plugin_system/core/component_registry.py | 62 +- src/plugin_system/core/plugin_manager.py | 11 +- .../built_in/NEW_napcat_adapter/plugin.py | 105 +- .../src/handlers/napcat_cache.json | 1 + .../src/handlers/to_core/message_handler.py | 38 +- .../built_in/napcat_adapter_plugin/.gitignore | 279 --- .../built_in/napcat_adapter_plugin/CONSTS.py | 254 --- .../napcat_adapter_plugin/__init__.py | 16 - .../napcat_adapter_plugin/event_handlers.py | 1779 ---------------- .../napcat_adapter_plugin/event_types.py | 1844 ----------------- .../built_in/napcat_adapter_plugin/plugin.py | 430 ---- .../napcat_adapter_plugin/pyproject.toml | 47 - .../napcat_adapter_plugin/src/__init__.py | 32 - .../napcat_adapter_plugin/src/database.py | 157 -- .../src/message_chunker.py | 280 --- .../src/mmc_com_layer.py | 61 - .../src/recv_handler/__init__.py | 92 - .../src/recv_handler/message_handler.py | 1183 ----------- .../src/recv_handler/message_sending.py | 120 -- .../src/recv_handler/meta_event_handler.py | 65 - .../src/recv_handler/notice_handler.py | 705 ------- .../src/recv_handler/qq_emoji_list.py | 252 --- .../src/response_pool.py | 63 - .../napcat_adapter_plugin/src/send_handler.py | 705 ------- .../src/stream_router.py | 351 ---- .../napcat_adapter_plugin/src/utils.py | 314 --- .../src/video_handler.py | 179 -- .../src/websocket_manager.py | 163 -- .../built_in/napcat_adapter_plugin/todo.md | 89 - 35 files changed, 162 insertions(+), 9624 deletions(-) create mode 100644 src/plugins/built_in/NEW_napcat_adapter/src/handlers/napcat_cache.json delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/.gitignore delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/CONSTS.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/__init__.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/event_handlers.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/event_types.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/plugin.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/pyproject.toml delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/__init__.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/database.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/message_chunker.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/stream_router.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/utils.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/video_handler.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/websocket_manager.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/todo.md diff --git a/src/chat/interest_system/bot_interest_manager.py b/src/chat/interest_system/bot_interest_manager.py index ada2d0365..e817426c1 100644 --- a/src/chat/interest_system/bot_interest_manager.py +++ b/src/chat/interest_system/bot_interest_manager.py @@ -45,7 +45,6 @@ class BotInterestManager: """初始化兴趣标签系统""" try: logger.info("机器人兴趣系统开始初始化...") - logger.info(f"人设ID: {personality_id}, 描述长度: {len(personality_description)}") # 初始化embedding模型 await self._initialize_embedding_model() @@ -74,19 +73,14 @@ class BotInterestManager: async def _initialize_embedding_model(self): """初始化embedding模型""" - logger.info("🔧 正在配置embedding客户端...") - # 使用项目配置的embedding模型 from src.config.config import model_config from src.llm_models.utils_model import LLMRequest - logger.debug("✅ 成功导入embedding相关模块") - # 检查embedding配置是否存在 if not hasattr(model_config.model_task_config, "embedding"): raise RuntimeError("❌ 未找到embedding模型配置") - logger.info("📋 找到embedding模型配置") self.embedding_config = model_config.model_task_config.embedding if self.embedding_dimension: @@ -96,36 +90,20 @@ class BotInterestManager: # 创建LLMRequest实例用于embedding self.embedding_request = LLMRequest(model_set=self.embedding_config, request_type="interest_embedding") - logger.info("✅ Embedding请求客户端初始化成功") - logger.info(f"🔗 客户端类型: {type(self.embedding_request).__name__}") - - # 获取第一个embedding模型的ModelInfo - if hasattr(self.embedding_config, "model_list") and self.embedding_config.model_list: - first_model_name = self.embedding_config.model_list[0] - logger.info(f"🎯 使用embedding模型: {first_model_name}") - else: - logger.warning("⚠️ 未找到embedding模型列表") - - logger.info("✅ Embedding模型初始化完成") async def _load_or_generate_interests(self, personality_description: str, personality_id: str): """加载或生成兴趣标签""" - logger.info(f"📚 正在为 '{personality_id}' 加载或生成兴趣标签...") # 首先尝试从数据库加载 - logger.info("尝试从数据库加载兴趣标签...") loaded_interests = await self._load_interests_from_database(personality_id) if loaded_interests: self.current_interests = loaded_interests - active_count = len(loaded_interests.get_active_tags()) - logger.info(f"成功从数据库加载 {active_count} 个兴趣标签 (版本: {loaded_interests.version})") + active_count = len(loaded_interests.get_active_tags()) tags_info = [f" - '{tag.tag_name}' (权重: {tag.weight:.2f})" for tag in loaded_interests.get_active_tags()] tags_str = "\n".join(tags_info) - logger.info(f"当前兴趣标签:\n{tags_str}") # 为加载的标签生成embedding(数据库不存储embedding,启动时动态生成) - logger.info("🧠 为加载的标签生成embedding向量...") await self._generate_embeddings_for_tags(loaded_interests) else: # 生成新的兴趣标签 @@ -163,7 +141,6 @@ class BotInterestManager: raise RuntimeError("❌ Embedding客户端未初始化,无法生成兴趣标签") # 构建提示词 - logger.info("📝 构建LLM提示词...") prompt = f""" 基于以下机器人人设描述,生成一套合适的兴趣标签: @@ -218,13 +195,11 @@ class BotInterestManager: """ # 调用LLM生成兴趣标签 - logger.info("🤖 正在调用LLM生成兴趣标签...") response = await self._call_llm_for_interest_generation(prompt) if not response: raise RuntimeError("❌ LLM未返回有效响应") - logger.info("✅ LLM响应成功,开始解析兴趣标签...") # 使用统一的 JSON 解析工具 interests_data = extract_and_parse_json(response, strict=False) if not interests_data or not isinstance(interests_data, dict): @@ -290,7 +265,6 @@ class BotInterestManager: replyer_config = model_config.model_task_config.replyer # 调用LLM API - logger.info("🚀 正在通过LLM API发送请求...") success, response, reasoning_content, model_name = await llm_api.generate_with_model( prompt=full_prompt, model_config=replyer_config, @@ -300,13 +274,6 @@ class BotInterestManager: ) if success and response: - logger.info(f"✅ LLM响应成功,模型: {model_name}, 响应长度: {len(response)} 字符") - logger.debug( - f"📄 LLM响应内容: {response[:200]}..." if len(response) > 200 else f"📄 LLM响应内容: {response}" - ) - if reasoning_content: - logger.debug(f"🧠 推理内容: {reasoning_content[:100]}...") - # 直接返回原始响应,后续使用统一的 JSON 解析工具 return response else: @@ -329,11 +296,8 @@ class BotInterestManager: # 尝试从文件加载缓存 file_cache = await self._load_embedding_cache_from_file(interests.personality_id) if file_cache: - logger.info(f"📂 从文件加载 {len(file_cache)} 个embedding缓存") self.embedding_cache.update(file_cache) - logger.info(f"🧠 开始为 {total_tags} 个兴趣标签生成embedding向量...") - memory_cached_count = 0 file_cached_count = 0 generated_count = 0 @@ -352,8 +316,6 @@ class BotInterestManager: else: # 动态生成新的embedding embedding_text = tag.tag_name - - logger.debug(f" [{i}/{total_tags}] 🔄 正在为 '{tag.tag_name}' 动态生成embedding...") embedding = await self._get_embedding(embedding_text) if embedding: @@ -371,18 +333,8 @@ class BotInterestManager: # 如果有新生成的embedding,保存到文件 if generated_count > 0: await self._save_embedding_cache_to_file(interests.personality_id) - logger.info(f"💾 已将 {generated_count} 个新生成的embedding保存到缓存文件") interests.last_updated = datetime.now() - logger.info("=" * 50) - logger.info("✅ Embedding生成完成!") - logger.info(f"📊 总标签数: {total_tags}") - logger.info(f"� 文件缓存命中: {file_cached_count}") - logger.info(f"�💾 内存缓存命中: {memory_cached_count}") - logger.info(f"🆕 新生成: {generated_count}") - logger.info(f"❌ 失败: {failed_count}") - logger.info(f"🗃️ 总缓存大小: {len(self.embedding_cache)}") - logger.info("=" * 50) async def _get_embedding(self, text: str) -> list[float]: """获取文本的embedding向量""" @@ -391,11 +343,9 @@ class BotInterestManager: # 检查缓存 if text in self.embedding_cache: - logger.debug(f"💾 使用缓存的embedding: '{text[:30]}...'") return self.embedding_cache[text] # 使用LLMRequest获取embedding - logger.debug(f"🔄 正在获取embedding: '{text[:30]}...'") if not self.embedding_request: raise RuntimeError("❌ Embedding客户端未初始化") embedding, model_name = await self.embedding_request.get_embedding(text) @@ -414,16 +364,12 @@ class BotInterestManager: ) else: self.embedding_dimension = current_dim - logger.info(f"📏 检测到embedding维度: {current_dim}") elif current_dim != self.embedding_dimension: logger.warning( "⚠️ 收到的embedding维度发生变化: 之前=%d, 当前=%d。请确认模型配置是否正确。", self.embedding_dimension, current_dim, ) - - logger.debug(f"✅ Embedding获取成功,维度: {current_dim}, 模型: {model_name}") - return embedding else: raise RuntimeError(f"❌ 返回的embedding为空: {embedding}") @@ -435,11 +381,8 @@ class BotInterestManager: else: combined_text = message_text - logger.debug(f"🔄 正在为消息生成embedding,输入长度: {len(combined_text)}") - # 生成embedding embedding = await self._get_embedding(combined_text) - logger.debug(f"✅ 消息embedding生成成功,维度: {len(embedding)}") return embedding async def generate_embeddings_for_texts( diff --git a/src/chat/message_receive/message_handler.py b/src/chat/message_receive/message_handler.py index ff40a51a5..362ab40f4 100644 --- a/src/chat/message_receive/message_handler.py +++ b/src/chat/message_receive/message_handler.py @@ -258,6 +258,10 @@ class MessageHandler: group_info = message_info.get("group_info") user_info = message_info.get("user_info") + if not user_info and not group_info: + logger.debug("消息缺少用户信息,已跳过处理") + return None + # 获取或创建聊天流 platform = message_info.get("platform", "unknown") diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index aaca76746..0bf545580 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -41,6 +41,7 @@ from .base import ( EventHandlerInfo, EventType, PluginInfo, + AdapterInfo, # 新增的增强命令系统 PlusCommand, BaseRouterComponent, diff --git a/src/plugin_system/base/base_adapter.py b/src/plugin_system/base/base_adapter.py index 88e65aa22..ac38d250f 100644 --- a/src/plugin_system/base/base_adapter.py +++ b/src/plugin_system/base/base_adapter.py @@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional from mofox_wire import AdapterBase as MoFoxAdapterBase, CoreSink, MessageEnvelope, ProcessCoreSink if TYPE_CHECKING: - from src.plugin_system.base.base_plugin import BasePlugin + from src.plugin_system import BasePlugin, AdapterInfo from src.common.logger import get_logger diff --git a/src/plugin_system/base/base_plugin.py b/src/plugin_system/base/base_plugin.py index ada78e634..f6f492bef 100644 --- a/src/plugin_system/base/base_plugin.py +++ b/src/plugin_system/base/base_plugin.py @@ -3,6 +3,7 @@ from abc import abstractmethod from src.common.logger import get_logger from src.plugin_system.base.component_types import ( ActionInfo, + AdapterInfo, CommandInfo, ComponentType, EventHandlerInfo, @@ -13,6 +14,7 @@ from src.plugin_system.base.component_types import ( ) from .base_action import BaseAction +from .base_adapter import BaseAdapter from .base_command import BaseCommand from .base_events_handler import BaseEventHandler from .base_interest_calculator import BaseInterestCalculator @@ -35,27 +37,26 @@ class BasePlugin(PluginBase): @classmethod def _get_component_info_from_class(cls, component_class: type, component_type: ComponentType): - """从组件类自动生成组件信息 + """从类获取组件信息 Args: component_class: 组件类 component_type: 组件类型 - Returns: - 对应类型的ComponentInfo对象 + 对应的ComponentInfo对象 """ if component_type == ComponentType.COMMAND: if hasattr(component_class, "get_command_info"): return component_class.get_command_info() else: - logger.warning(f"Command类 {component_class.__name__} 缺少 get_command_info 方法") + logger.warning(f"Command组件 {component_class.__name__} 缺少 get_command_info 方法") return None elif component_type == ComponentType.ACTION: if hasattr(component_class, "get_action_info"): return component_class.get_action_info() else: - logger.warning(f"Action类 {component_class.__name__} 缺少 get_action_info 方法") + logger.warning(f"Action组件 {component_class.__name__} 缺少 get_action_info 方法") return None elif component_type == ComponentType.INTEREST_CALCULATOR: @@ -63,30 +64,37 @@ class BasePlugin(PluginBase): return component_class.get_interest_calculator_info() else: logger.warning( - f"InterestCalculator类 {component_class.__name__} 缺少 get_interest_calculator_info 方法" + f"InterestCalculator组件 {component_class.__name__} 缺少 get_interest_calculator_info 方法" ) return None elif component_type == ComponentType.PLUS_COMMAND: - # PlusCommand的get_info逻辑可以在这里实现 - logger.warning("PlusCommand的get_info逻辑尚未实现") + # PlusCommand组件的get_info方法尚未实现 + logger.warning("PlusCommand组件的get_info方法尚未实现") return None elif component_type == ComponentType.TOOL: - # Tool的get_info逻辑可以在这里实现 - logger.warning("Tool的get_info逻辑尚未实现") + # Tool组件的get_info方法尚未实现 + logger.warning("Tool组件的get_info方法尚未实现") return None elif component_type == ComponentType.EVENT_HANDLER: - # EventHandler的get_info逻辑可以在这里实现 - logger.warning("EventHandler的get_info逻辑尚未实现") + # EventHandler组件的get_info方法尚未实现 + logger.warning("EventHandler组件的get_info方法尚未实现") return None elif component_type == ComponentType.PROMPT: if hasattr(component_class, "get_prompt_info"): return component_class.get_prompt_info() else: - logger.warning(f"Prompt类 {component_class.__name__} 缺少 get_prompt_info 方法") + logger.warning(f"Prompt组件 {component_class.__name__} 缺少 get_prompt_info 方法") + return None + + elif component_type == ComponentType.ADAPTER: + if hasattr(component_class, "get_adapter_info"): + return component_class.get_adapter_info() + else: + logger.warning(f"Adapter�� {component_class.__name__} ȱ�� get_adapter_info ����") return None else: @@ -95,16 +103,13 @@ class BasePlugin(PluginBase): @classmethod def get_component_info(cls, component_class: type, component_type: ComponentType): - """获取组件信息的通用方法 - - 这是一个便捷方法,内部调用_get_component_info_from_class + """获取组件信息 Args: component_class: 组件类 component_type: 组件类型 - Returns: - 对应类型的ComponentInfo对象 + 对应的ComponentInfo对象 """ return cls._get_component_info_from_class(component_class, component_type) @@ -113,6 +118,7 @@ class BasePlugin(PluginBase): self, ) -> list[ tuple[ActionInfo, type[BaseAction]] + | tuple[AdapterInfo, type[BaseAdapter]] | tuple[CommandInfo, type[BaseCommand]] | tuple[PlusCommandInfo, type[PlusCommand]] | tuple[EventHandlerInfo, type[BaseEventHandler]] diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index becbe1b63..b60ac9789 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -152,6 +152,7 @@ class AdapterInfo: name: str # 适配器名称 component_type: ComponentType = field(default=ComponentType.ADAPTER, init=False) + plugin_name: str = "" # ����������� version: str = "1.0.0" # 适配器版本 platform: str = "unknown" # 平台名称 description: str = "" # 适配器描述 diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index d40cb358c..9fc78caf0 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -10,6 +10,7 @@ from fastapi import Depends from src.common.logger import get_logger from src.config.config import global_config as bot_config from src.plugin_system.base.base_action import BaseAction +from src.plugin_system.base.base_adapter import BaseAdapter from src.plugin_system.base.base_chatter import BaseChatter from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_events_handler import BaseEventHandler @@ -19,6 +20,7 @@ from src.plugin_system.base.base_prompt import BasePrompt from src.plugin_system.base.base_tool import BaseTool from src.plugin_system.base.component_types import ( ActionInfo, + AdapterInfo, ChatterInfo, CommandInfo, ComponentInfo, @@ -46,6 +48,7 @@ ComponentClassType = ( | type[BaseInterestCalculator] | type[BasePrompt] | type[BaseRouterComponent] + | type[BaseAdapter] ) @@ -109,6 +112,21 @@ class ComponentRegistry: """启用的chatter名 -> chatter类""" logger.info("组件注册中心初始化完成") + self._interest_calculator_registry: dict[str, type["BaseInterestCalculator"]] = {} + """兴趣计算器名 -> 兴趣计算器类""" + self._enabled_interest_calculator_registry: dict[str, type["BaseInterestCalculator"]] = {} + """启用的兴趣计算器名 -> 兴趣计算器类""" + + self._prompt_registry: dict[str, type["BasePrompt"]] = {} + """提示词组件名 -> 提示词组件类""" + self._enabled_prompt_registry: dict[str, type["BasePrompt"]] = {} + """启用的提示词组件名 -> 提示词组件类""" + + self._adapter_registry: dict[str, type["BaseAdapter"]] = {} + """适配器组件名 -> 适配器组件类""" + self._enabled_adapter_registry: dict[str, type["BaseAdapter"]] = {} + """启用的适配器组件名 -> 适配器组件类""" + # == 注册方法 == def register_plugin(self, plugin_info: PluginInfo) -> bool: @@ -204,6 +222,10 @@ class ComponentRegistry: assert isinstance(component_info, RouterInfo) assert issubclass(component_class, BaseRouterComponent) ret = self._register_router_component(component_info, component_class) + case ComponentType.ADAPTER: + assert isinstance(component_info, AdapterInfo) + assert issubclass(component_class, BaseAdapter) + ret = self._register_adapter_component(component_info, component_class) case _: logger.warning(f"未知组件类型: {component_type}") ret = False @@ -335,12 +357,6 @@ class ComponentRegistry: logger.error(f"注册失败: {calculator_name} 不是有效的InterestCalculator") return False - # 创建专门的InterestCalculator注册表(如果还没有) - if not hasattr(self, "_interest_calculator_registry"): - self._interest_calculator_registry: dict[str, type["BaseInterestCalculator"]] = {} - if not hasattr(self, "_enabled_interest_calculator_registry"): - self._enabled_interest_calculator_registry: dict[str, type["BaseInterestCalculator"]] = {} - _assign_plugin_attrs( interest_calculator_class, interest_calculator_info.plugin_name, @@ -365,11 +381,6 @@ class ComponentRegistry: logger.error(f"Prompt组件 {prompt_class.__name__} 必须指定名称") return False - if not hasattr(self, "_prompt_registry"): - self._prompt_registry: dict[str, type[BasePrompt]] = {} - if not hasattr(self, "_enabled_prompt_registry"): - self._enabled_prompt_registry: dict[str, type[BasePrompt]] = {} - _assign_plugin_attrs( prompt_class, prompt_info.plugin_name, self.get_plugin_config(prompt_info.plugin_name) or {} ) @@ -420,6 +431,29 @@ class ComponentRegistry: logger.error(f"注册路由组件 '{router_info.name}' 时出错: {e}", exc_info=True) return False + def _register_adapter_component(self, adapter_info: AdapterInfo, adapter_class: type[BaseAdapter]) -> bool: + """将Adapter组件注册到Adapter特定注册表""" + + adapter_name = adapter_info.name + if not adapter_name: + logger.error(f"Adapter组件 {adapter_class.__name__} 必须指定名称") + return False + if not isinstance(adapter_info, AdapterInfo) or not issubclass(adapter_class, BaseAdapter): + logger.error(f"注册失败: {adapter_name} 不是有效的Adapter") + return False + + _assign_plugin_attrs( + adapter_class, adapter_info.plugin_name, self.get_plugin_config(adapter_info.plugin_name) or {} + ) + if not hasattr(self, "_adapter_registry"): + self._adapter_registry: dict[str, type[BaseAdapter]] = {} + + self._adapter_registry[adapter_name] = adapter_class + + if not adapter_info.enabled: + logger.warning(f"Adapter {adapter_name} 未启用") + return True + # === 组件移除相关 === async def remove_component(self, component_name: str, component_type: ComponentType, plugin_name: str) -> bool: @@ -664,6 +698,7 @@ class ComponentRegistry: | BaseInterestCalculator | BasePrompt | BaseRouterComponent + | BaseAdapter ] | None ): @@ -693,6 +728,7 @@ class ComponentRegistry: | type[BaseInterestCalculator] | type[BasePrompt] | type[BaseRouterComponent] + | type[BaseAdapter] | None, self._components_classes.get(namespaced_name), ) @@ -919,6 +955,7 @@ class ComponentRegistry: chatter_components: int = 0 prompt_components: int = 0 router_components: int = 0 + adapter_components: int = 0 for component in self._components.values(): if component.component_type == ComponentType.ACTION: action_components += 1 @@ -936,6 +973,8 @@ class ComponentRegistry: prompt_components += 1 elif component.component_type == ComponentType.ROUTER: router_components += 1 + elif component.component_type == ComponentType.ADAPTER: + adapter_components += 1 return { "action_components": action_components, "command_components": command_components, @@ -946,6 +985,7 @@ class ComponentRegistry: "chatter_components": chatter_components, "prompt_components": prompt_components, "router_components": router_components, + "adapter_components": adapter_components, "total_components": len(self._components), "total_plugins": len(self._plugins), "components_by_type": { diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 125fac63e..49f864b45 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -481,13 +481,14 @@ class PluginManager: chatter_count = stats.get("chatter_components", 0) prompt_count = stats.get("prompt_components", 0) router_count = stats.get("router_components", 0) + adapter_count = stats.get("adapter_components", 0) total_components = stats.get("total_components", 0) # 📋 显示插件加载总览 if total_registered > 0: logger.info("🎉 插件系统加载完成!") logger.info( - f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, Tool: {tool_count}, PlusCommand: {plus_command_count}, EventHandler: {event_handler_count}, Chatter: {chatter_count}, Prompt: {prompt_count}, Router: {router_count})" + f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, Tool: {tool_count}, PlusCommand: {plus_command_count}, EventHandler: {event_handler_count}, Chatter: {chatter_count}, Prompt: {prompt_count}, Router: {router_count}, Adapter: {adapter_count})" ) # 显示详细的插件列表 @@ -531,6 +532,9 @@ class PluginManager: router_components = [ c for c in plugin_info.components if c.component_type == ComponentType.ROUTER ] + adapter_components = [ + c for c in plugin_info.components if c.component_type == ComponentType.ADAPTER + ] if action_components: action_details = [format_component(c) for c in action_components] @@ -560,6 +564,9 @@ class PluginManager: if router_components: router_details = [format_component(c) for c in router_components] logger.info(f" 🌐 Router组件: {', '.join(router_details)}") + if adapter_components: + adapter_details = [format_component(c) for c in adapter_components] + logger.info(f" 🔌 Adapter组件: {', '.join(adapter_details)}") # 权限节点信息 if plugin_instance := self.loaded_plugins.get(plugin_name): @@ -698,4 +705,4 @@ class PluginManager: # 全局插件管理器实例 -plugin_manager = PluginManager() \ No newline at end of file +plugin_manager = PluginManager() diff --git a/src/plugins/built_in/NEW_napcat_adapter/plugin.py b/src/plugins/built_in/NEW_napcat_adapter/plugin.py index 1eefc990b..be4b8a2dd 100644 --- a/src/plugins/built_in/NEW_napcat_adapter/plugin.py +++ b/src/plugins/built_in/NEW_napcat_adapter/plugin.py @@ -19,7 +19,7 @@ import websockets from mofox_wire import CoreSink, MessageEnvelope, WebSocketAdapterOptions from src.common.logger import get_logger -from src.plugin_system import register_plugin +from src.plugin_system import ConfigField, register_plugin from src.plugin_system.base import BaseAdapter, BasePlugin from src.plugin_system.apis import config_api @@ -43,7 +43,7 @@ class NapcatAdapter(BaseAdapter): run_in_subprocess = False - def __init__(self, core_sink: CoreSink, plugin: Optional[BasePlugin] = None): + def __init__(self, core_sink: CoreSink, plugin: Optional[BasePlugin] = None, **kwargs): """初始化 Napcat 适配器""" # 从插件配置读取 WebSocket URL if plugin: @@ -52,7 +52,6 @@ class NapcatAdapter(BaseAdapter): access_token = config_api.get_plugin_config(plugin.config, "napcat_server.access_token", "") ws_url = f"ws://{host}:{port}" - headers = {} if access_token: headers["Authorization"] = f"Bearer {access_token}" @@ -62,11 +61,12 @@ class NapcatAdapter(BaseAdapter): # 配置 WebSocket 传输 transport = WebSocketAdapterOptions( + mode="server", url=ws_url, headers=headers if headers else None, ) - super().__init__(core_sink, plugin=plugin, transport=transport) + super().__init__(core_sink, plugin=plugin, transport=transport, **kwargs) # 初始化处理器 self.message_handler = MessageHandler(self) @@ -178,11 +178,14 @@ class NapcatAdapter(BaseAdapter): self._response_pool[echo] = future # 构造请求 - request = orjson.dumps({ - "action": action, - "params": params, - "echo": echo, - }) + # Napcat expects JSON text frames; orjson.dumps returns bytes so decode to str + request = orjson.dumps( + { + "action": action, + "params": params, + "echo": echo, + } + ).decode() try: # 发送请求 @@ -214,57 +217,53 @@ class NapcatAdapterPlugin(BasePlugin): """Napcat 适配器插件""" plugin_name = "napcat_adapter_plugin" + config_file_name = "config.toml" enable_plugin = True plugin_version = "2.0.0" plugin_author = "MoFox Team" plugin_description = "Napcat/OneBot 11 适配器(基于 MoFox-Bus 重写)" - # 配置 Schema - config_schema: ClassVar[dict] = { - "plugin": { - "name": {"type": str, "default": "napcat_adapter_plugin"}, - "version": {"type": str, "default": "1.0.0"}, - "enabled": {"type": bool, "default": True}, - }, - "napcat_server": { - "host": {"type": str, "default": "localhost"}, - "port": {"type": int, "default": 8095}, - "access_token": {"type": str, "default": ""}, - }, - "features": { - "group_list_type": {"type": str, "default": "blacklist"}, - "group_list": {"type": list, "default": []}, - "private_list_type": {"type": str, "default": "blacklist"}, - "private_list": {"type": list, "default": []}, - "ban_user_id": {"type": list, "default": []}, - "ban_qq_bot": {"type": bool, "default": False}, - }, + config_section_descriptions: ClassVar = { + "plugin": "插件开关", + "napcat_server": "Napcat WebSocket 连接设置", + "features": "过滤和名单配置", } - def __init__(self): - self._adapter: Optional[NapcatAdapter] = None - - async def on_plugin_loaded(self): - """插件加载时启动适配器""" - logger.info("Napcat 适配器插件正在加载...") - - # 从 CoreSinkManager 获取 InProcessCoreSink - from src.common.core_sink_manager import get_core_sink_manager - - core_sink_manager = get_core_sink_manager() - core_sink = core_sink_manager.get_in_process_sink() - - # 创建并启动适配器 - self._adapter = NapcatAdapter(core_sink, plugin=self) - await self._adapter.start() - - logger.info("Napcat 适配器插件已加载") - - async def on_plugin_unloaded(self): - """插件卸载时停止适配器""" - if self._adapter: - await self._adapter.stop() - logger.info("Napcat 适配器插件已卸载") + config_schema: ClassVar[dict] = { + "plugin": { + "enabled": ConfigField(type=bool, default=True, description="是否启用 Napcat 适配器"), + "config_version": ConfigField(type=str, default="2.0.0", description="配置文件版本"), + }, + "napcat_server": { + "mode": ConfigField( + type=str, + default="reverse", + description="ws 连接模式: reverse/direct", + choices=["reverse", "direct"], + ), + "host": ConfigField(type=str, default="localhost", description="Napcat WebSocket 服务地址"), + "port": ConfigField(type=int, default=8095, description="Napcat WebSocket 服务端口"), + "access_token": ConfigField(type=str, default="", description="Napcat API 访问令牌(可选)"), + }, + "features": { + "group_list_type": ConfigField( + type=str, + default="blacklist", + description="群聊名单模式: blacklist/whitelist", + choices=["blacklist", "whitelist"], + ), + "group_list": ConfigField(type=list, default=[], description="群聊名单;根据名单模式过滤"), + "private_list_type": ConfigField( + type=str, + default="blacklist", + description="私聊名单模式: blacklist/whitelist", + choices=["blacklist", "whitelist"], + ), + "private_list": ConfigField(type=list, default=[], description="私聊名单;根据名单模式过滤"), + "ban_user_id": ConfigField(type=list, default=[], description="全局封禁的用户 ID 列表"), + "ban_qq_bot": ConfigField(type=bool, default=False, description="是否屏蔽其他 QQ 机器人消息"), + }, + } def get_plugin_components(self) -> list: """返回适配器组件""" diff --git a/src/plugins/built_in/NEW_napcat_adapter/src/handlers/napcat_cache.json b/src/plugins/built_in/NEW_napcat_adapter/src/handlers/napcat_cache.json new file mode 100644 index 000000000..6e642ef3d --- /dev/null +++ b/src/plugins/built_in/NEW_napcat_adapter/src/handlers/napcat_cache.json @@ -0,0 +1 @@ +{"group_info":{"169850076":{"data":{"group_all_shut":0,"group_remark":"","group_id":169850076,"group_name":"墨狐狐🌟起源之地","member_count":439,"max_member_count":500},"ts":1764145931.0869653},"329577252":{"data":{"group_all_shut":0,"group_remark":"","group_id":329577252,"group_name":"真言的小窝『小可爱聚集地』","member_count":1748,"max_member_count":2000},"ts":1764145933.5440657},"160791652":{"data":{"group_all_shut":0,"group_remark":"","group_id":160791652,"group_name":"一群「往世乐土」","member_count":476,"max_member_count":500},"ts":1764146234.340923}},"group_detail_info":{},"member_info":{"329577252:3307339367":{"data":{"group_id":329577252,"user_id":3307339367,"nickname":"爱丽丝","card":"爱丽丝","sex":"unknown","age":0,"area":"","level":"70","qq_level":11,"join_time":1751672499,"last_sent_time":1764145599,"title_expire_time":0,"unfriendly":false,"card_changeable":true,"is_robot":false,"shut_up_timestamp":0,"role":"admin","title":"不智能ai"},"ts":1764145619.5129182},"169850076:2359337932":{"data":{"group_id":169850076,"user_id":2359337932,"nickname":"不到人","card":"","sex":"unknown","age":0,"area":"","level":"43","qq_level":56,"join_time":1758019556,"last_sent_time":1764145519,"title_expire_time":0,"unfriendly":false,"card_changeable":true,"is_robot":false,"shut_up_timestamp":0,"role":"admin","title":""},"ts":1764145931.3597307}},"stranger_info":{},"self_info":{}} \ No newline at end of file diff --git a/src/plugins/built_in/NEW_napcat_adapter/src/handlers/to_core/message_handler.py b/src/plugins/built_in/NEW_napcat_adapter/src/handlers/to_core/message_handler.py index 0e83e39bb..abe00a10c 100644 --- a/src/plugins/built_in/NEW_napcat_adapter/src/handlers/to_core/message_handler.py +++ b/src/plugins/built_in/NEW_napcat_adapter/src/handlers/to_core/message_handler.py @@ -106,11 +106,13 @@ class MessageHandler: accept_format=ACCEPT_FORMAT, ) + msg_builder.seg_list(seg_list) + return msg_builder.build() async def handle_single_segment( self, segment: dict, raw_message: dict, in_reply: bool = False - ) -> SegPayload | List[SegPayload] | None: + ) -> SegPayload | None: """ 处理单一消息段并转换为 MessageEnvelope @@ -203,31 +205,25 @@ class MessageHandler: sender_info: dict = message_detail.get("sender", {}) sender_nickname: str = sender_info.get("nickname", "") sender_id = sender_info.get("user_id") - seg_message: List[SegPayload] = [] if not sender_nickname: logger.warning("无法获取被引用的人的昵称,返回默认值") - seg_message.append( - { - "type": "text", - "data": f"[回复<未知用户>:{reply_message}],说:", - } - ) + return { + "type": "text", + "data": f"[回复<未知用户>:{reply_message}],说:", + } + else: if sender_id: - seg_message.append( - { - "type": "text", - "data": f"[回复<{sender_nickname}({sender_id})>:{reply_message}],说:", - } - ) + return { + "type": "text", + "data": f"[回复<{sender_nickname}({sender_id})>:{reply_message}],说:", + } else: - seg_message.append( - { - "type": "text", - "data": f"[回复<{sender_nickname}>:{reply_message}],说:", - } - ) - return seg_message + return { + "type": "text", + "data": f"[回复<{sender_nickname}>:{reply_message}],说:", + } + case "voice": seg_data = segment.get("url", "") case _: diff --git a/src/plugins/built_in/napcat_adapter_plugin/.gitignore b/src/plugins/built_in/napcat_adapter_plugin/.gitignore deleted file mode 100644 index e4ce10662..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/.gitignore +++ /dev/null @@ -1,279 +0,0 @@ - -log/ -logs/ -out/ - -.env -.env.* -.cursor - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class -uv.lock -llm_statistics.txt -mongodb -napcat -run_dev.bat -elua.confirmed -# C extensions -*.so -/results -config_backup/ -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# PyPI configuration file -.pypirc - -# rjieba -rjieba.cache - -# .vscode -!.vscode/settings.json - -# direnv -/.direnv - -# JetBrains -.idea -*.iml -*.ipr - -# PyEnv -# If using PyEnv and configured to use a specific Python version locally -# a .local-version file will be created in the root of the project to specify the version. -.python-version - -OtherRes.txt - -/eula.confirmed -/privacy.confirmed - -logs - -.ruff_cache - -.vscode - -/config/* -config/old/bot_config_20250405_212257.toml -temp/ - -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -config.toml -feature.toml -config.toml.back -test -data/NapcatAdapter.db -data/NapcatAdapter.db-shm -data/NapcatAdapter.db-wal \ No newline at end of file diff --git a/src/plugins/built_in/napcat_adapter_plugin/CONSTS.py b/src/plugins/built_in/napcat_adapter_plugin/CONSTS.py deleted file mode 100644 index aac12589d..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/CONSTS.py +++ /dev/null @@ -1,254 +0,0 @@ -PLUGIN_NAME = "napcat_adapter" - -QQ_FACE: dict = { - "0": "[表情:惊讶]", - "1": "[表情:撇嘴]", - "2": "[表情:色]", - "3": "[表情:发呆]", - "4": "[表情:得意]", - "5": "[表情:流泪]", - "6": "[表情:害羞]", - "7": "[表情:闭嘴]", - "8": "[表情:睡]", - "9": "[表情:大哭]", - "10": "[表情:尴尬]", - "11": "[表情:发怒]", - "12": "[表情:调皮]", - "13": "[表情:呲牙]", - "14": "[表情:微笑]", - "15": "[表情:难过]", - "16": "[表情:酷]", - "18": "[表情:抓狂]", - "19": "[表情:吐]", - "20": "[表情:偷笑]", - "21": "[表情:可爱]", - "22": "[表情:白眼]", - "23": "[表情:傲慢]", - "24": "[表情:饥饿]", - "25": "[表情:困]", - "26": "[表情:惊恐]", - "27": "[表情:流汗]", - "28": "[表情:憨笑]", - "29": "[表情:悠闲]", - "30": "[表情:奋斗]", - "31": "[表情:咒骂]", - "32": "[表情:疑问]", - "33": "[表情: 嘘]", - "34": "[表情:晕]", - "35": "[表情:折磨]", - "36": "[表情:衰]", - "37": "[表情:骷髅]", - "38": "[表情:敲打]", - "39": "[表情:再见]", - "41": "[表情:发抖]", - "42": "[表情:爱情]", - "43": "[表情:跳跳]", - "46": "[表情:猪头]", - "49": "[表情:拥抱]", - "53": "[表情:蛋糕]", - "56": "[表情:刀]", - "59": "[表情:便便]", - "60": "[表情:咖啡]", - "63": "[表情:玫瑰]", - "64": "[表情:凋谢]", - "66": "[表情:爱心]", - "67": "[表情:心碎]", - "74": "[表情:太阳]", - "75": "[表情:月亮]", - "76": "[表情:赞]", - "77": "[表情:踩]", - "78": "[表情:握手]", - "79": "[表情:胜利]", - "85": "[表情:飞吻]", - "86": "[表情:怄火]", - "89": "[表情:西瓜]", - "96": "[表情:冷汗]", - "97": "[表情:擦汗]", - "98": "[表情:抠鼻]", - "99": "[表情:鼓掌]", - "100": "[表情:糗大了]", - "101": "[表情:坏笑]", - "102": "[表情:左哼哼]", - "103": "[表情:右哼哼]", - "104": "[表情:哈欠]", - "105": "[表情:鄙视]", - "106": "[表情:委屈]", - "107": "[表情:快哭了]", - "108": "[表情:阴险]", - "109": "[表情:左亲亲]", - "110": "[表情:吓]", - "111": "[表情:可怜]", - "112": "[表情:菜刀]", - "114": "[表情:篮球]", - "116": "[表情:示爱]", - "118": "[表情:抱拳]", - "119": "[表情:勾引]", - "120": "[表情:拳头]", - "121": "[表情:差劲]", - "123": "[表情:NO]", - "124": "[表情:OK]", - "125": "[表情:转圈]", - "129": "[表情:挥手]", - "137": "[表情:鞭炮]", - "144": "[表情:喝彩]", - "146": "[表情:爆筋]", - "147": "[表情:棒棒糖]", - "169": "[表情:手枪]", - "171": "[表情:茶]", - "172": "[表情:眨眼睛]", - "173": "[表情:泪奔]", - "174": "[表情:无奈]", - "175": "[表情:卖萌]", - "176": "[表情:小纠结]", - "177": "[表情:喷血]", - "178": "[表情:斜眼笑]", - "179": "[表情:doge]", - "181": "[表情:戳一戳]", - "182": "[表情:笑哭]", - "183": "[表情:我最美]", - "185": "[表情:羊驼]", - "187": "[表情:幽灵]", - "201": "[表情:点赞]", - "212": "[表情:托腮]", - "262": "[表情:脑阔疼]", - "263": "[表情:沧桑]", - "264": "[表情:捂脸]", - "265": "[表情:辣眼睛]", - "266": "[表情:哦哟]", - "267": "[表情:头秃]", - "268": "[表情:问号脸]", - "269": "[表情:暗中观察]", - "270": "[表情:emm]", - "271": "[表情:吃 瓜]", - "272": "[表情:呵呵哒]", - "273": "[表情:我酸了]", - "277": "[表情:滑稽狗头]", - "281": "[表情:翻白眼]", - "282": "[表情:敬礼]", - "283": "[表情:狂笑]", - "284": "[表情:面无表情]", - "285": "[表情:摸鱼]", - "286": "[表情:魔鬼笑]", - "287": "[表情:哦]", - "289": "[表情:睁眼]", - "293": "[表情:摸锦鲤]", - "294": "[表情:期待]", - "295": "[表情:拿到红包]", - "297": "[表情:拜谢]", - "298": "[表情:元宝]", - "299": "[表情:牛啊]", - "300": "[表情:胖三斤]", - "302": "[表情:左拜年]", - "303": "[表情:右拜年]", - "305": "[表情:右亲亲]", - "306": "[表情:牛气冲天]", - "307": "[表情:喵喵]", - "311": "[表情:打call]", - "312": "[表情:变形]", - "314": "[表情:仔细分析]", - "317": "[表情:菜汪]", - "318": "[表情:崇拜]", - "319": "[表情: 比心]", - "320": "[表情:庆祝]", - "323": "[表情:嫌弃]", - "324": "[表情:吃糖]", - "325": "[表情:惊吓]", - "326": "[表情:生气]", - "332": "[表情:举牌牌]", - "333": "[表情:烟花]", - "334": "[表情:虎虎生威]", - "336": "[表情:豹富]", - "337": "[表情:花朵脸]", - "338": "[表情:我想开了]", - "339": "[表情:舔屏]", - "341": "[表情:打招呼]", - "342": "[表情:酸Q]", - "343": "[表情:我方了]", - "344": "[表情:大怨种]", - "345": "[表情:红包多多]", - "346": "[表情:你真棒棒]", - "347": "[表情:大展宏兔]", - "349": "[表情:坚强]", - "350": "[表情:贴贴]", - "351": "[表情:敲敲]", - "352": "[表情:咦]", - "353": "[表情:拜托]", - "354": "[表情:尊嘟假嘟]", - "355": "[表情:耶]", - "356": "[表情:666]", - "357": "[表情:裂开]", - "392": "[表情:龙年 快乐]", - "393": "[表情:新年中龙]", - "394": "[表情:新年大龙]", - "395": "[表情:略略略]", - "396": "[表情:龙年快乐]", - "424": "[表情:按钮]", - "😊": "[表情:嘿嘿]", - "😌": "[表情:羞涩]", - "😚": "[ 表情:亲亲]", - "😓": "[表情:汗]", - "😰": "[表情:紧张]", - "😝": "[表情:吐舌]", - "😁": "[表情:呲牙]", - "😜": "[表情:淘气]", - "☺": "[表情:可爱]", - "😍": "[表情:花痴]", - "😔": "[表情:失落]", - "😄": "[表情:高兴]", - "😏": "[表情:哼哼]", - "😒": "[表情:不屑]", - "😳": "[表情:瞪眼]", - "😘": "[表情:飞吻]", - "😭": "[表情:大哭]", - "😱": "[表情:害怕]", - "😂": "[表情:激动]", - "💪": "[表情:肌肉]", - "👊": "[表情:拳头]", - "👍": "[表情 :厉害]", - "👏": "[表情:鼓掌]", - "👎": "[表情:鄙视]", - "🙏": "[表情:合十]", - "👌": "[表情:好的]", - "👆": "[表情:向上]", - "👀": "[表情:眼睛]", - "🍜": "[表情:拉面]", - "🍧": "[表情:刨冰]", - "🍞": "[表情:面包]", - "🍺": "[表情:啤酒]", - "🍻": "[表情:干杯]", - "☕": "[表情:咖啡]", - "🍎": "[表情:苹果]", - "🍓": "[表情:草莓]", - "🍉": "[表情:西瓜]", - "🚬": "[表情:吸烟]", - "🌹": "[表情:玫瑰]", - "🎉": "[表情:庆祝]", - "💝": "[表情:礼物]", - "💣": "[表情:炸弹]", - "✨": "[表情:闪光]", - "💨": "[表情:吹气]", - "💦": "[表情:水]", - "🔥": "[表情:火]", - "💤": "[表情:睡觉]", - "💩": "[表情:便便]", - "💉": "[表情:打针]", - "📫": "[表情:邮箱]", - "🐎": "[表情:骑马]", - "👧": "[表情:女孩]", - "👦": "[表情:男孩]", - "🐵": "[表情:猴]", - "🐷": "[表情:猪]", - "🐮": "[表情:牛]", - "🐔": "[表情:公鸡]", - "🐸": "[表情:青蛙]", - "👻": "[表情:幽灵]", - "🐛": "[表情:虫]", - "🐶": "[表情:狗]", - "🐳": "[表情:鲸鱼]", - "👢": "[表情:靴子]", - "☀": "[表情:晴天]", - "❔": "[表情:问号]", - "🔫": "[表情:手枪]", - "💓": "[表情:爱 心]", - "🏪": "[表情:便利店]", -} diff --git a/src/plugins/built_in/napcat_adapter_plugin/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/__init__.py deleted file mode 100644 index 15e1543e6..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from src.plugin_system.base.plugin_metadata import PluginMetadata - -__plugin_meta__ = PluginMetadata( - name="napcat_plugin", - description="基于OneBot 11协议的NapCat QQ协议插件,提供完整的QQ机器人API接口,使用现有adapter连接", - usage="该插件提供 `napcat_tool` tool。", - version="1.0.0", - author="Windpicker_owo", - license="GPL-v3.0-or-later", - repository_url="https://github.com/Windpicker-owo/InternetSearchPlugin", - keywords=["qq", "bot", "napcat", "onebot", "api", "websocket"], - categories=["protocol"], - extra={ - "is_built_in": False, - }, -) diff --git a/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py b/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py deleted file mode 100644 index 5855237d2..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py +++ /dev/null @@ -1,1779 +0,0 @@ -from typing import ClassVar - -from src.common.logger import get_logger -from src.plugin_system import BaseEventHandler -from src.plugin_system.base.base_event import HandlerResult - -from .event_types import NapcatEvent -from .src.send_handler import send_handler - -logger = get_logger("napcat_adapter") - - -class SetProfileHandler(BaseEventHandler): - handler_name: str = "napcat_set_qq_profile_handler" - handler_description: str = "设置账号信息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.SET_PROFILE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - nickname = params.get("nickname", "") - personal_note = params.get("personal_note", "") - sex = params.get("sex", "") - - if params.get("raw", ""): - nickname = raw.get("nickname", "") - personal_note = raw.get("personal_note", "") - sex = raw.get("sex", "") - - if not nickname: - logger.error("事件 napcat_set_qq_profile 缺少必要参数: nickname ") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"nickname": nickname, "personal_note": personal_note, "sex": sex} - response = await send_handler.send_message_to_napcat(action="set_qq_profile", params=payload) - if response.get("status", "") == "ok": - if response.get("data", "").get("result", "") == 0: - return HandlerResult(True, True, response) - else: - logger.error(f"事件 napcat_set_qq_profile 请求失败!err={response.get('data', '').get('errMsg', '')}") - return HandlerResult(False, False, response) - else: - logger.error("事件 napcat_set_qq_profile 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetOnlineClientsHandler(BaseEventHandler): - handler_name: str = "napcat_get_online_clients_handler" - handler_description: str = "获取当前账号在线客户端列表" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_ONLINE_CLIENTS] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - no_cache = params.get("no_cache", False) - - if params.get("raw", ""): - no_cache = raw.get("no_cache", False) - - payload = {"no_cache": no_cache} - response = await send_handler.send_message_to_napcat(action="get_online_clients", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_online_clients 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetOnlineStatusHandler(BaseEventHandler): - handler_name: str = "napcat_set_online_status_handler" - handler_description: str = "设置在线状态" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.SET_ONLINE_STATUS] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - status = params.get("status", "") - ext_status = params.get("ext_status", "0") - battery_status = params.get("battery_status", "0") - - if params.get("raw", ""): - status = raw.get("status", "") - ext_status = raw.get("ext_status", "0") - battery_status = raw.get("battery_status", "0") - - if not status: - logger.error("事件 napcat_set_online_status 缺少必要参数: status") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"status": status, "ext_status": ext_status, "battery_status": battery_status} - response = await send_handler.send_message_to_napcat(action="set_online_status", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_online_status 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetFriendsWithCategoryHandler(BaseEventHandler): - handler_name: str = "napcat_get_friends_with_category_handler" - handler_description: str = "获取好友分组列表" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_FRIENDS_WITH_CATEGORY] - - async def execute(self, params: dict): - payload = {} - response = await send_handler.send_message_to_napcat(action="get_friends_with_category", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_friends_with_category 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetAvatarHandler(BaseEventHandler): - handler_name: str = "napcat_set_qq_avatar_handler" - handler_description: str = "设置头像" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.SET_AVATAR] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - file = params.get("file", "") - - if params.get("raw", ""): - file = raw.get("file", "") - - if not file: - logger.error("事件 napcat_set_qq_avatar 缺少必要参数: file") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"file": file} - response = await send_handler.send_message_to_napcat(action="set_qq_avatar", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_qq_avatar 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SendLikeHandler(BaseEventHandler): - handler_name: str = "napcat_send_like_handler" - handler_description: str = "点赞" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.SEND_LIKE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - times = params.get("times", 1) - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - times = raw.get("times", 1) - - if not user_id: - logger.error("事件 napcat_send_like 缺少必要参数: user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"user_id": str(user_id), "times": times} - response = await send_handler.send_message_to_napcat(action="send_like", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_send_like 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetFriendAddRequestHandler(BaseEventHandler): - handler_name: str = "napcat_set_friend_add_request_handler" - handler_description: str = "处理好友请求" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.SET_FRIEND_ADD_REQUEST] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - flag = params.get("flag", "") - approve = params.get("approve", True) - remark = params.get("remark", "") - - if params.get("raw", ""): - flag = raw.get("flag", "") - approve = raw.get("approve", True) - remark = raw.get("remark", "") - - if not flag or approve is None or remark is None: - logger.error("事件 napcat_set_friend_add_request 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"flag": flag, "approve": approve, "remark": remark} - response = await send_handler.send_message_to_napcat(action="set_friend_add_request", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_friend_add_request 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetSelfLongnickHandler(BaseEventHandler): - handler_name: str = "napcat_set_self_longnick_handler" - handler_description: str = "设置个性签名" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.SET_SELF_LONGNICK] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - longNick = params.get("longNick", "") - - if params.get("raw", ""): - longNick = raw.get("longNick", "") - - if not longNick: - logger.error("事件 napcat_set_self_longnick 缺少必要参数: longNick") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"longNick": longNick} - response = await send_handler.send_message_to_napcat(action="set_self_longnick", params=payload) - if response.get("status", "") == "ok": - if response.get("data", {}).get("result", "") == 0: - return HandlerResult(True, True, response) - else: - logger.error( - f"事件 napcat_set_self_longnick 请求失败!err={response.get('data', {}).get('errMsg', '')}" - ) - return HandlerResult(False, False, response) - else: - logger.error("事件 napcat_set_self_longnick 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetLoginInfoHandler(BaseEventHandler): - handler_name: str = "napcat_get_login_info_handler" - handler_description: str = "获取登录号信息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_LOGIN_INFO] - - async def execute(self, params: dict): - payload = {} - response = await send_handler.send_message_to_napcat(action="get_login_info", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_login_info 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetRecentContactHandler(BaseEventHandler): - handler_name: str = "napcat_get_recent_contact_handler" - handler_description: str = "最近消息列表" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_RECENT_CONTACT] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - count = params.get("count", 20) - - if params.get("raw", ""): - count = raw.get("count", 20) - - payload = {"count": count} - response = await send_handler.send_message_to_napcat(action="get_recent_contact", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_recent_contact 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetStrangerInfoHandler(BaseEventHandler): - handler_name: str = "napcat_get_stranger_info_handler" - handler_description: str = "获取(指定)账号信息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_STRANGER_INFO] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - - if not user_id: - logger.error("事件 napcat_get_stranger_info 缺少必要参数: user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"user_id": str(user_id)} - response = await send_handler.send_message_to_napcat(action="get_stranger_info", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_stranger_info 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetFriendListHandler(BaseEventHandler): - handler_name: str = "napcat_get_friend_list_handler" - handler_description: str = "获取好友列表" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_FRIEND_LIST] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - no_cache = params.get("no_cache", False) - - if params.get("raw", ""): - no_cache = raw.get("no_cache", False) - - payload = {"no_cache": no_cache} - response = await send_handler.send_message_to_napcat(action="get_friend_list", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_friend_list 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetProfileLikeHandler(BaseEventHandler): - handler_name: str = "napcat_get_profile_like_handler" - handler_description: str = "获取点赞列表" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_PROFILE_LIKE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - start = params.get("start", 0) - count = params.get("count", 10) - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - start = raw.get("start", 0) - count = raw.get("count", 10) - - payload = {"start": start, "count": count} - if user_id: - payload["user_id"] = str(user_id) - - response = await send_handler.send_message_to_napcat(action="get_profile_like", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_profile_like 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class DeleteFriendHandler(BaseEventHandler): - handler_name: str = "napcat_delete_friend_handler" - handler_description: str = "删除好友" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.DELETE_FRIEND] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - temp_block = params.get("temp_block", False) - temp_both_del = params.get("temp_both_del", False) - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - temp_block = raw.get("temp_block", False) - temp_both_del = raw.get("temp_both_del", False) - - if not user_id or temp_block is None or temp_both_del is None: - logger.error("事件 napcat_delete_friend 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"user_id": str(user_id), "temp_block": temp_block, "temp_both_del": temp_both_del} - response = await send_handler.send_message_to_napcat(action="delete_friend", params=payload) - if response.get("status", "") == "ok": - if response.get("data", {}).get("result", "") == 0: - return HandlerResult(True, True, response) - else: - logger.error(f"事件 napcat_delete_friend 请求失败!err={response.get('data', {}).get('errMsg', '')}") - return HandlerResult(False, False, response) - else: - logger.error("事件 napcat_delete_friend 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetUserStatusHandler(BaseEventHandler): - handler_name: str = "napcat_get_user_status_handler" - handler_description: str = "获取(指定)用户状态" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_USER_STATUS] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - - if not user_id: - logger.error("事件 napcat_get_user_status 缺少必要参数: user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"user_id": str(user_id)} - response = await send_handler.send_message_to_napcat(action="get_user_status", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_user_status 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetStatusHandler(BaseEventHandler): - handler_name: str = "napcat_get_status_handler" - handler_description: str = "获取状态" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_STATUS] - - async def execute(self, params: dict): - payload = {} - response = await send_handler.send_message_to_napcat(action="get_status", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_status 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetMiniAppArkHandler(BaseEventHandler): - handler_name: str = "napcat_get_mini_app_ark_handler" - handler_description: str = "获取小程序卡片" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.GET_MINI_APP_ARK] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - type = params.get("type", "") - title = params.get("title", "") - desc = params.get("desc", "") - picUrl = params.get("picUrl", "") - jumpUrl = params.get("jumpUrl", "") - webUrl = params.get("webUrl", "") - rawArkData = params.get("rawArkData", False) - - if params.get("raw", ""): - type = raw.get("type", "") - title = raw.get("title", "") - desc = raw.get("desc", "") - picUrl = raw.get("picUrl", "") - jumpUrl = raw.get("jumpUrl", "") - webUrl = raw.get("webUrl", "") - rawArkData = raw.get("rawArkData", False) - - if not type or not title or not desc or not picUrl or not jumpUrl: - logger.error("事件 napcat_get_mini_app_ark 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = { - "type": type, - "title": title, - "desc": desc, - "picUrl": picUrl, - "jumpUrl": jumpUrl, - "webUrl": webUrl, - "rawArkData": rawArkData, - } - response = await send_handler.send_message_to_napcat(action="get_mini_app_ark", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_mini_app_ark 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetDiyOnlineStatusHandler(BaseEventHandler): - handler_name: str = "napcat_set_diy_online_status_handler" - handler_description: str = "设置自定义在线状态" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.ACCOUNT.SET_DIY_ONLINE_STATUS] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - face_id = params.get("face_id", "") - face_type = params.get("face_type", "0") - wording = params.get("wording", "") - - if params.get("raw", ""): - face_id = raw.get("face_id", "") - face_type = raw.get("face_type", "0") - wording = raw.get("wording", "") - - if not face_id: - logger.error("事件 napcat_set_diy_online_status 缺少必要参数: face_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"face_id": str(face_id), "face_type": str(face_type), "wording": wording} - response = await send_handler.send_message_to_napcat(action="set_diy_online_status", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_diy_online_status 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -# ===MESSAGE=== -class SendPrivateMsgHandler(BaseEventHandler): - handler_name: str = "napcat_send_private_msg_handler" - handler_description: str = "发送私聊消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.SEND_PRIVATE_MSG] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - message = params.get("message", "") - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - message = raw.get("message", "") - - if not user_id or not message: - logger.error("事件 napcat_send_private_msg 缺少必要参数: user_id 或 message") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"user_id": str(user_id), "message": message} - response = await send_handler.send_message_to_napcat(action="send_private_msg", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_send_private_msg 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SendPokeHandler(BaseEventHandler): - handler_name: str = "napcat_send_poke_handler" - handler_description: str = "发送戳一戳" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.SEND_POKE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - group_id = params.get("group_id", None) - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - group_id = raw.get("group_id", None) - - if not user_id: - logger.error("事件 napcat_send_poke 缺少必要参数: user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"user_id": str(user_id)} - if group_id is not None: - payload["group_id"] = str(group_id) - - response = await send_handler.send_message_to_napcat(action="send_poke", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_send_poke 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class DeleteMsgHandler(BaseEventHandler): - handler_name: str = "napcat_delete_msg_handler" - handler_description: str = "撤回消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.DELETE_MSG] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - message_id = params.get("message_id", "") - - if params.get("raw", ""): - message_id = raw.get("message_id", "") - - if not message_id: - logger.error("事件 napcat_delete_msg 缺少必要参数: message_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"message_id": str(message_id)} - response = await send_handler.send_message_to_napcat(action="delete_msg", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_delete_msg 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupMsgHistoryHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_msg_history_handler" - handler_description: str = "获取群历史消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.GET_GROUP_MSG_HISTORY] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - message_seq = params.get("message_seq", 0) - count = params.get("count", 20) - reverseOrder = params.get("reverseOrder", False) - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - message_seq = raw.get("message_seq", 0) - count = raw.get("count", 20) - reverseOrder = raw.get("reverseOrder", False) - - if not group_id: - logger.error("事件 napcat_get_group_msg_history 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = { - "group_id": str(group_id), - "message_seq": int(message_seq), - "count": int(count), - "reverseOrder": bool(reverseOrder), - } - response = await send_handler.send_message_to_napcat(action="get_group_msg_history", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_msg_history 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetMsgHandler(BaseEventHandler): - handler_name: str = "napcat_get_msg_handler" - handler_description: str = "获取消息详情" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.GET_MSG] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - message_id = params.get("message_id", "") - - if params.get("raw", ""): - message_id = raw.get("message_id", "") - - if not message_id: - logger.error("事件 napcat_get_msg 缺少必要参数: message_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"message_id": str(message_id)} - response = await send_handler.send_message_to_napcat(action="get_msg", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_msg 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetForwardMsgHandler(BaseEventHandler): - handler_name: str = "napcat_get_forward_msg_handler" - handler_description: str = "获取合并转发消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.GET_FORWARD_MSG] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - message_id = params.get("message_id", "") - - if params.get("raw", ""): - message_id = raw.get("message_id", "") - - if not message_id: - logger.error("事件 napcat_get_forward_msg 缺少必要参数: message_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"message_id": str(message_id)} - response = await send_handler.send_message_to_napcat(action="get_forward_msg", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_forward_msg 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetMsgEmojiLikeHandler(BaseEventHandler): - handler_name: str = "napcat_set_msg_emoji_like_handler" - handler_description: str = "贴表情" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.SET_MSG_EMOJI_LIKE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - message_id = params.get("message_id", "") - emoji_id = params.get("emoji_id", 0) - set_flag = params.get("set", True) - - if params.get("raw", ""): - message_id = raw.get("message_id", "") - emoji_id = raw.get("emoji_id", 0) - set_flag = raw.get("set", True) - - if not message_id or emoji_id is None or set_flag is None: - logger.error("事件 napcat_set_msg_emoji_like 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"message_id": str(message_id), "emoji_id": int(emoji_id), "set": bool(set_flag)} - response = await send_handler.send_message_to_napcat(action="set_msg_emoji_like", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_msg_emoji_like 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetFriendMsgHistoryHandler(BaseEventHandler): - handler_name: str = "napcat_get_friend_msg_history_handler" - handler_description: str = "获取好友历史消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.GET_FRIEND_MSG_HISTORY] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - message_seq = params.get("message_seq", 0) - count = params.get("count", 20) - reverseOrder = params.get("reverseOrder", False) - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - message_seq = raw.get("message_seq", 0) - count = raw.get("count", 20) - reverseOrder = raw.get("reverseOrder", False) - - if not user_id: - logger.error("事件 napcat_get_friend_msg_history 缺少必要参数: user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = { - "user_id": str(user_id), - "message_seq": int(message_seq), - "count": int(count), - "reverseOrder": bool(reverseOrder), - } - response = await send_handler.send_message_to_napcat(action="get_friend_msg_history", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_friend_msg_history 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class FetchEmojiLikeHandler(BaseEventHandler): - handler_name: str = "napcat_fetch_emoji_like_handler" - handler_description: str = "获取贴表情详情" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.FETCH_EMOJI_LIKE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - message_id = params.get("message_id", "") - emoji_id = params.get("emoji_id", "") - emoji_type = params.get("emoji_type", "") - count = params.get("count", 20) - - if params.get("raw", ""): - message_id = raw.get("message_id", "") - emoji_id = raw.get("emoji_id", "") - emoji_type = raw.get("emoji_type", "") - count = raw.get("count", 20) - - if not message_id or not emoji_id or not emoji_type: - logger.error("事件 napcat_fetch_emoji_like 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = { - "message_id": str(message_id), - "emojiId": str(emoji_id), - "emojiType": str(emoji_type), - "count": int(count), - } - response = await send_handler.send_message_to_napcat(action="fetch_emoji_like", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_fetch_emoji_like 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SendForwardMsgHandler(BaseEventHandler): - handler_name: str = "napcat_send_forward_msg_handler" - handler_description: str = "发送合并转发消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.SEND_FORWARD_MSG] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - messages = params.get("messages", {}) - news = params.get("news", {}) - prompt = params.get("prompt", "") - summary = params.get("summary", "") - source = params.get("source", "") - group_id = params.get("group_id", None) - user_id = params.get("user_id", None) - - if params.get("raw", ""): - messages = raw.get("messages", {}) - news = raw.get("news", {}) - prompt = raw.get("prompt", "") - summary = raw.get("summary", "") - source = raw.get("source", "") - group_id = raw.get("group_id", None) - user_id = raw.get("user_id", None) - - if not messages or not news or not prompt or not summary or not source: - logger.error("事件 napcat_send_forward_msg 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"messages": messages, "news": news, "prompt": prompt, "summary": summary, "source": source} - if group_id is not None: - payload["group_id"] = str(group_id) - if user_id is not None: - payload["user_id"] = str(user_id) - - response = await send_handler.send_message_to_napcat(action="send_forward_msg", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_send_forward_msg 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SendGroupAiRecordHandler(BaseEventHandler): - handler_name: str = "napcat_send_group_ai_record_handler" - handler_description: str = "发送群AI语音" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.MESSAGE.SEND_GROUP_AI_RECORD] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - character = params.get("character", "") - text = params.get("text", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - character = raw.get("character", "") - text = raw.get("text", "") - - if not group_id or not character or not text: - logger.error("事件 napcat_send_group_ai_record 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "character": character, "text": text} - response = await send_handler.send_message_to_napcat(action="send_group_ai_record", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_send_group_ai_record 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -# ===GROUP=== -class GetGroupInfoHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_info_handler" - handler_description: str = "获取群信息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_INFO] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - - if not group_id: - logger.error("事件 napcat_get_group_info 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - response = await send_handler.send_message_to_napcat(action="get_group_info", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_info 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupAddOptionHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_add_option_handler" - handler_description: str = "设置群添加选项" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_ADD_OPTION] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - add_type = params.get("add_type", "") - group_question = params.get("group_question", "") - group_answer = params.get("group_answer", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - add_type = raw.get("add_type", "") - group_question = raw.get("group_question", "") - group_answer = raw.get("group_answer", "") - - if not group_id or not add_type: - logger.error("事件 napcat_set_group_add_option 缺少必要参数: group_id 或 add_type") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "add_type": str(add_type)} - if group_question: - payload["group_question"] = group_question - if group_answer: - payload["group_answer"] = group_answer - - response = await send_handler.send_message_to_napcat(action="set_group_add_option", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_add_option 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupKickMembersHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_kick_members_handler" - handler_description: str = "批量踢出群成员" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_KICK_MEMBERS] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - user_id = params.get("user_id", []) - reject_add_request = params.get("reject_add_request", False) - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - user_id = raw.get("user_id", []) - reject_add_request = raw.get("reject_add_request", False) - - if not group_id or not user_id: - logger.error("事件 napcat_set_group_kick_members 缺少必要参数: group_id 或 user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "user_id": user_id, "reject_add_request": bool(reject_add_request)} - response = await send_handler.send_message_to_napcat(action="set_group_kick_members", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_kick_members 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupRemarkHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_remark_handler" - handler_description: str = "设置群备注" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_REMARK] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - remark = params.get("remark", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - remark = raw.get("remark", "") - - if not group_id or not remark: - logger.error("事件 napcat_set_group_remark 缺少必要参数: group_id 或 remark") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "remark": remark} - response = await send_handler.send_message_to_napcat(action="set_group_remark", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_remark 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupKickHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_kick_handler" - handler_description: str = "群踢人" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_KICK] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - user_id = params.get("user_id", "") - reject_add_request = params.get("reject_add_request", False) - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - user_id = raw.get("user_id", "") - reject_add_request = raw.get("reject_add_request", False) - - if not group_id or not user_id: - logger.error("事件 napcat_set_group_kick 缺少必要参数: group_id 或 user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "user_id": str(user_id), "reject_add_request": bool(reject_add_request)} - response = await send_handler.send_message_to_napcat(action="set_group_kick", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_kick 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupSystemMsgHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_system_msg_handler" - handler_description: str = "获取群系统消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_SYSTEM_MSG] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - count = params.get("count", 20) - - if params.get("raw", ""): - count = raw.get("count", 20) - - if count is None: - logger.error("事件 napcat_get_group_system_msg 缺少必要参数: count") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"count": int(count)} - response = await send_handler.send_message_to_napcat(action="get_group_system_msg", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_system_msg 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupBanHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_ban_handler" - handler_description: str = "群禁言" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_BAN] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - user_id = params.get("user_id", "") - duration = params.get("duration", 0) - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - user_id = raw.get("user_id", "") - duration = raw.get("duration", 0) - - if not group_id or not user_id or duration is None: - logger.error("事件 napcat_set_group_ban 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "user_id": str(user_id), "duration": int(duration)} - response = await send_handler.send_message_to_napcat(action="set_group_ban", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_ban 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetEssenceMsgListHandler(BaseEventHandler): - handler_name: str = "napcat_get_essence_msg_list_handler" - handler_description: str = "获取群精华消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_ESSENCE_MSG_LIST] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - - if not group_id: - logger.error("事件 napcat_get_essence_msg_list 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - response = await send_handler.send_message_to_napcat(action="get_essence_msg_list", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_essence_msg_list 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupWholeBanHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_whole_ban_handler" - handler_description: str = "全体禁言" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_WHOLE_BAN] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - enable = params.get("enable", True) - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - enable = raw.get("enable", True) - - if not group_id or enable is None: - logger.error("事件 napcat_set_group_whole_ban 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "enable": bool(enable)} - response = await send_handler.send_message_to_napcat(action="set_group_whole_ban", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_whole_ban 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupPortraitHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_portrait_handler" - handler_description: str = "设置群头像" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_PORTRAINT] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - file_path = params.get("file", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - file_path = raw.get("file", "") - - if not group_id or not file_path: - logger.error("事件 napcat_set_group_portrait 缺少必要参数: group_id 或 file") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "file": file_path} - response = await send_handler.send_message_to_napcat(action="set_group_portrait", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_portrait 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupAdminHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_admin_handler" - handler_description: str = "设置群管理" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_ADMIN] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - user_id = params.get("user_id", "") - enable = params.get("enable", True) - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - user_id = raw.get("user_id", "") - enable = raw.get("enable", True) - - if not group_id or not user_id or enable is None: - logger.error("事件 napcat_set_group_admin 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "user_id": str(user_id), "enable": bool(enable)} - response = await send_handler.send_message_to_napcat(action="set_group_admin", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_admin 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupCardHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_card_handler" - handler_description: str = "设置群成员名片" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_CARD] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - user_id = params.get("user_id", "") - card = params.get("card", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - user_id = raw.get("user_id", "") - card = raw.get("card", "") - - if not group_id or not user_id: - logger.error("事件 napcat_set_group_card 缺少必要参数: group_id 或 user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "user_id": str(user_id)} - if card: - payload["card"] = card - - response = await send_handler.send_message_to_napcat(action="group_card", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_card 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetEssenceMsgHandler(BaseEventHandler): - handler_name: str = "napcat_set_essence_msg_handler" - handler_description: str = "设置群精华消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_ESSENCE_MSG] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - message_id = params.get("message_id", "") - - if params.get("raw", ""): - message_id = raw.get("message_id", "") - - if not message_id: - logger.error("事件 napcat_set_essence_msg 缺少必要参数: message_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"message_id": str(message_id)} - response = await send_handler.send_message_to_napcat(action="set_essence_msg", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_essence_msg 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupNameHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_name_handler" - handler_description: str = "设置群名" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_NAME] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - group_name = params.get("group_name", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - group_name = raw.get("group_name", "") - - if not group_id or not group_name: - logger.error("事件 napcat_set_group_name 缺少必要参数: group_id 或 group_name") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "group_name": group_name} - response = await send_handler.send_message_to_napcat(action="set_group_name", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_name 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class DeleteEssenceMsgHandler(BaseEventHandler): - handler_name: str = "napcat_delete_essence_msg_handler" - handler_description: str = "删除群精华消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.DELETE_ESSENCE_MSG] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - message_id = params.get("message_id", "") - - if params.get("raw", ""): - message_id = raw.get("message_id", "") - - if not message_id: - logger.error("事件 napcat_delete_essence_msg 缺少必要参数: message_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"message_id": str(message_id)} - response = await send_handler.send_message_to_napcat(action="delete_essence_msg", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_delete_essence_msg 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupLeaveHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_leave_handler" - handler_description: str = "退群" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_LEAVE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - - if not group_id: - logger.error("事件 napcat_set_group_leave 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - response = await send_handler.send_message_to_napcat(action="set_group_leave", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_leave 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SendGroupNoticeHandler(BaseEventHandler): - handler_name: str = "napcat_send_group_notice_handler" - handler_description: str = "发送群公告" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SEND_GROUP_NOTICE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - content = params.get("content", "") - image = params.get("image", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - content = raw.get("content", "") - image = raw.get("image", "") - - if not group_id or not content: - logger.error("事件 napcat_send_group_notice 缺少必要参数: group_id 或 content") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "content": content} - if image: - payload["image"] = image - - response = await send_handler.send_message_to_napcat(action="_send_group_notice", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_send_group_notice 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupSpecialTitleHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_special_title_handler" - handler_description: str = "设置群头衔" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_SPECIAL_TITLE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - user_id = params.get("user_id", "") - special_title = params.get("special_title", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - user_id = raw.get("user_id", "") - special_title = raw.get("special_title", "") - - if not group_id or not user_id: - logger.error("事件 napcat_set_group_special_title 缺少必要参数: group_id 或 user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "user_id": str(user_id)} - if special_title: - payload["special_title"] = special_title - - response = await send_handler.send_message_to_napcat(action="set_group_special_title", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_special_title 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupNoticeHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_notice_handler" - handler_description: str = "获取群公告" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_NOTICE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - - if not group_id: - logger.error("事件 napcat_get_group_notice 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - response = await send_handler.send_message_to_napcat(action="_get_group_notice", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_notice 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupAddRequestHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_add_request_handler" - handler_description: str = "处理加群请求" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_ADD_REQUEST] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - flag = params.get("flag", "") - approve = params.get("approve", True) - reason = params.get("reason", "") - - if params.get("raw", ""): - flag = raw.get("flag", "") - approve = raw.get("approve", True) - reason = raw.get("reason", "") - - if not flag or approve is None: - logger.error("事件 napcat_set_group_add_request 缺少必要参数") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"flag": flag, "approve": bool(approve)} - if reason: - payload["reason"] = reason - - response = await send_handler.send_message_to_napcat(action="set_group_add_request", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_add_request 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupListHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_list_handler" - handler_description: str = "获取群列表" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_LIST] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - no_cache = params.get("no_cache", False) - - if params.get("raw", ""): - no_cache = raw.get("no_cache", False) - - payload = {"no_cache": bool(no_cache)} - response = await send_handler.send_message_to_napcat(action="get_group_list", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_list 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class DeleteGroupNoticeHandler(BaseEventHandler): - handler_name: str = "napcat_del_group_notice_handler" - handler_description: str = "删除群公告" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.DELETE_GROUP_NOTICE] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - notice_id = params.get("notice_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - notice_id = raw.get("notice_id", "") - - if not group_id or not notice_id: - logger.error("事件 napcat_del_group_notice 缺少必要参数: group_id 或 notice_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "notice_id": notice_id} - response = await send_handler.send_message_to_napcat(action="_del_group_notice", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_del_group_notice 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupMemberInfoHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_member_info_handler" - handler_description: str = "获取群成员信息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_MEMBER_INFO] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - user_id = params.get("user_id", "") - no_cache = params.get("no_cache", False) - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - user_id = raw.get("user_id", "") - no_cache = raw.get("no_cache", False) - - if not group_id or not user_id: - logger.error("事件 napcat_get_group_member_info 缺少必要参数: group_id 或 user_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "user_id": str(user_id), "no_cache": bool(no_cache)} - response = await send_handler.send_message_to_napcat(action="get_group_member_info", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_member_info 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupMemberListHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_member_list_handler" - handler_description: str = "获取群成员列表" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_MEMBER_LIST] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - no_cache = params.get("no_cache", False) - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - no_cache = raw.get("no_cache", False) - - if not group_id: - logger.error("事件 napcat_get_group_member_list 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id), "no_cache": bool(no_cache)} - response = await send_handler.send_message_to_napcat(action="get_group_member_list", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_member_list 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupHonorInfoHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_honor_info_handler" - handler_description: str = "获取群荣誉" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_HONOR_INFO] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - type = params.get("type", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - type = raw.get("type", "") - - if not group_id: - logger.error("事件 napcat_get_group_honor_info 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - if type: - payload["type"] = type - - response = await send_handler.send_message_to_napcat(action="get_group_honor_info", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_honor_info 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupInfoExHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_info_ex_handler" - handler_description: str = "获取群信息ex" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_INFO_EX] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - - if not group_id: - logger.error("事件 napcat_get_group_info_ex 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - response = await send_handler.send_message_to_napcat(action="get_group_info_ex", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_info_ex 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupAtAllRemainHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_at_all_remain_handler" - handler_description: str = "获取群 @全体成员 剩余次数" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_AT_ALL_REMAIN] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - - if not group_id: - logger.error("事件 napcat_get_group_at_all_remain 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - response = await send_handler.send_message_to_napcat(action="get_group_at_all_remain", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_at_all_remain 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupShutListHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_shut_list_handler" - handler_description: str = "获取群禁言列表" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_SHUT_LIST] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - - if not group_id: - logger.error("事件 napcat_get_group_shut_list 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - response = await send_handler.send_message_to_napcat(action="get_group_shut_list", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_shut_list 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class GetGroupIgnoredNotifiesHandler(BaseEventHandler): - handler_name: str = "napcat_get_group_ignored_notifies_handler" - handler_description: str = "获取群过滤系统消息" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.GET_GROUP_IGNORED_NOTIFIES] - - async def execute(self, params: dict): - payload = {} - response = await send_handler.send_message_to_napcat(action="get_group_ignored_notifies", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_get_group_ignored_notifies 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -class SetGroupSignHandler(BaseEventHandler): - handler_name: str = "napcat_set_group_sign_handler" - handler_description: str = "群打卡" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.GROUP.SET_GROUP_SIGN] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - group_id = params.get("group_id", "") - - if params.get("raw", ""): - group_id = raw.get("group_id", "") - - if not group_id: - logger.error("事件 napcat_set_group_sign 缺少必要参数: group_id") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"group_id": str(group_id)} - response = await send_handler.send_message_to_napcat(action="set_group_sign", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_group_sign 请求失败!") - return HandlerResult(False, False, {"status": "error"}) - - -# ===PERSONAL=== -class SetInputStatusHandler(BaseEventHandler): - handler_name: str = "napcat_set_input_status_handler" - handler_description: str = "设置输入状态" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [NapcatEvent.PERSONAL.SET_INPUT_STATUS] - - async def execute(self, params: dict): - raw = params.get("raw", {}) - user_id = params.get("user_id", "") - event_type = params.get("event_type", 0) - - if params.get("raw", ""): - user_id = raw.get("user_id", "") - event_type = raw.get("event_type", 0) - - if not user_id or event_type is None: - logger.error("事件 napcat_set_input_status 缺少必要参数: user_id 或 event_type") - return HandlerResult(False, False, {"status": "error"}) - - payload = {"user_id": str(user_id), "event_type": int(event_type)} - response = await send_handler.send_message_to_napcat(action="set_input_status", params=payload) - if response.get("status", "") == "ok": - return HandlerResult(True, True, response) - else: - logger.error("事件 napcat_set_input_status 请求失败!") - return HandlerResult(False, False, {"status": "error"}) diff --git a/src/plugins/built_in/napcat_adapter_plugin/event_types.py b/src/plugins/built_in/napcat_adapter_plugin/event_types.py deleted file mode 100644 index 08ef35598..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/event_types.py +++ /dev/null @@ -1,1844 +0,0 @@ -from enum import Enum - - -class NapcatEvent: - """ - napcat插件事件枚举类 - """ - - class ON_RECEIVED(Enum): - """ - 该分类下均为消息接受事件,只能由napcat_plugin触发 - """ - - TEXT = "napcat_on_received_text" - """接收到文本消息""" - FACE = "napcat_on_received_face" - """接收到表情消息""" - REPLY = "napcat_on_received_reply" - """接收到回复消息""" - IMAGE = "napcat_on_received_image" - """接收到图像消息""" - RECORD = "napcat_on_received_record" - """接收到语音消息""" - VIDEO = "napcat_on_received_video" - """接收到视频消息""" - AT = "napcat_on_received_at" - """接收到at消息""" - DICE = "napcat_on_received_dice" - """接收到骰子消息""" - SHAKE = "napcat_on_received_shake" - """接收到屏幕抖动消息""" - JSON = "napcat_on_received_json" - """接收到JSON消息""" - RPS = "napcat_on_received_rps" - """接收到魔法猜拳消息""" - FRIEND_INPUT = "napcat_on_friend_input" - """好友正在输入""" - EMOJI_LIEK = "napcat_on_received_emoji_like" - """接收到群聊表情回复""" - - class ACCOUNT(Enum): - """ - 该分类是对账户相关的操作,只能由外部触发,napcat_plugin负责处理 - """ - - SET_PROFILE = "napcat_set_qq_profile" - """设置账号信息 - - Args: - nickname (Optional[str]): 名称(必须) - personal_note (Optional[str]): 个性签名 - sex ('0'|'1'|'2'): 性别 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "result": 0, - "errMsg": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - - """ - GET_ONLINE_CLIENTS = "napcat_get_online_clients" - """获取当前账号在线客户端列表 - - Args: - no_cache (Optional[bool]): 是否不使用缓存 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - "string" - ], - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_ONLINE_STATUS = "napcat_set_online_status" - """设置在线状态 - - Args: - status (Optional[str]): 状态代码(必须) - ext_status (Optional[str]): 额外状态代码,默认为0 - battery_status (Optional[str]): 电池信息,默认为0 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_FRIENDS_WITH_CATEGORY = "napcat_get_friends_with_category" - """获取好友分组列表 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - { - "categoryId": 0, - "categorySortId": 0, - "categoryName": "string", - "categoryMbCount": 0, - "onlineCount": 0, - "buddyList": [ - { - "birthday_year": 0, - "birthday_month": 0, - "birthday_day": 0, - "user_id": 0, - "age": 0, - "phone_num": "string", - "email": "string", - "category_id": 0, - "nickname": "string", - "remark": "string", - "sex": "string", - "level": 0 - } - ] - } - ], - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_AVATAR = "napcat_set_qq_avatar" - """设置头像 - - Args: - file (Optional[str]): 文件路径或base64(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SEND_LIKE = "napcat_send_like" - """点赞 - - Args: - user_id (Optional[str|int]): 用户id(必需) - times (Optional[int]): 点赞次数,默认1 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_FRIEND_ADD_REQUEST = "napcat_set_friend_add_request" - """处理好友请求 - - Args: - flag (Optional[str]): 请求id(必需) - approve (Optional[bool]): 是否同意(必需) - remark (Optional[str]): 好友备注(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_SELF_LONGNICK = "napcat_set_self_longnick" - """设置个性签名 - - Args: - longNick (Optional[str]): 内容(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "result": 0, - "errMsg": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_LOGIN_INFO = "napcat_get_login_info" - """获取登录号信息 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "user_id": 0, - "nickname": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_RECENT_CONTACT = "napcat_get_recent_contact" - """最近消息列表 - - Args: - count (Optional[int]): 会话数量 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - { - "lastestMsg": { - "self_id": 0, - "user_id": 0, - "time": 0, - "real_seq": "string", - "message_type": "string", - "sender": { - "user_id": 0, - "nickname": "string", - "sex": "male", - "age": 0, - "card": "string", - "role": "owner" - }, - "raw_message": "string", - "font": 0, - "sub_type": "string", - "message": [ - { - "type": "text", - "data": { - "text": "string" - } - } - ], - "message_format": "string", - "post_type": "string", - "group_id": 0 - }, - "peerUin": "string", - "remark": "string", - "msgTime": "string", - "chatType": 0, - "msgId": "string", - "sendNickName": "string", - "sendMemberName": "string", - "peerName": "string" - } - ], - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_STRANGER_INFO = "napcat_get_stranger_info" - """获取(指定)账号信息 - - Args: - user_id (Optional[str|int]): 用户id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "user_id": 0, - "uid": "string", - "uin": "string", - "nickname": "string", - "age": 0, - "qid": "string", - "qqLevel": 0, - "sex": "string", - "long_nick": "string", - "reg_time": 0, - "is_vip": true, - "is_years_vip": true, - "vip_level": 0, - "remark": "string", - "status": 0, - "login_days": 0 - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_FRIEND_LIST = "napcat_get_friend_list" - """获取好友列表 - - Args: - no_cache (Optional[bool]): 是否不使用缓存 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - { - "birthday_year": 0, - "birthday_month": 0, - "birthday_day": 0, - "user_id": 0, - "age": 0, - "phone_num": "string", - "email": "string", - "category_id": 0, - "nickname": "string", - "remark": "string", - "sex": "string", - "level": 0 - } - ], - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_PROFILE_LIKE = "napcat_get_profile_like" - """获取点赞列表 - - Args: - user_id (Optional[str|int]): 用户id,指定用户,不填为获取所有 - start (Optional[int]): 起始值 - count (Optional[int]): 返回数量 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "uid": "string", - "time": 0, - "favoriteInfo": { - "total_count": 0, - "last_time": 0, - "today_count": 0, - "userInfos": [ - { - "uid": "string", - "src": 0, - "latestTime": 0, - "count": 0, - "giftCount": 0, - "customId": 0, - "lastCharged": 0, - "bAvailableCnt": 0, - "bTodayVotedCnt": 0, - "nick": "string", - "gender": 0, - "age": 0, - "isFriend": true, - "isvip": true, - "isSvip": true, - "uin": 0 - } - ] - }, - "voteInfo": { - "total_count": 0, - "new_count": 0, - "new_nearby_count": 0, - "last_visit_time": 0, - "userInfos": [ - { - "uid": "string", - "src": 0, - "latestTime": 0, - "count": 0, - "giftCount": 0, - "customId": 0, - "lastCharged": 0, - "bAvailableCnt": 0, - "bTodayVotedCnt": 0, - "nick": "string", - "gender": 0, - "age": 0, - "isFriend": true, - "isvip": true, - "isSvip": true, - "uin": 0 - } - ] - } - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - DELETE_FRIEND = "napcat_delete_friend" - """删除好友 - - Args: - user_id (Optional[str|int]): 用户id(必需) - temp_block (Optional[bool]): 拉黑(必需) - temp_both_del (Optional[bool]): 双向删除(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "result": 0, - "errMsg": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_USER_STATUS = "napcat_get_user_status" - """获取(指定)用户状态 - - Args: - user_id (Optional[str|int]): 用户id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "status": 0, - "ext_status": 0 - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_STATUS = "napcat_get_status" - """获取状态 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "online": true, - "good": true, - "stat": {} - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_MINI_APP_ARK = "napcat_get_mini_app_ark" - """获取小程序卡片 - - Args: - type (Optional[str]): 类型(如bili、weibo,必需) - title (Optional[str]): 标题(必需) - desc (Optional[str]): 描述(必需) - picUrl (Optional[str]): 图片URL(必需) - jumpUrl (Optional[str]): 跳转URL(必需) - webUrl (Optional[str]): 网页URL - rawArkData (Optional[bool]): 是否返回原始ark数据 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "appName": "string", - "appView": "string", - "ver": "string", - "desc": "string", - "prompt": "string", - "metaData": { - "detail_1": { - "appid": "string", - "appType": 0, - "title": "string", - "desc": "string", - "icon": "string", - "preview": "string", - "url": "string", - "scene": 0, - "host": { - "uin": 0, - "nick": "string" - }, - "shareTemplateId": "string", - "shareTemplateData": {}, - "showLittleTail": "string", - "gamePoints": "string", - "gamePointsUrl": "string", - "shareOrigin": 0 - } - }, - "config": { - "type": "string", - "width": 0, - "height": 0, - "forward": 0, - "autoSize": 0, - "ctime": 0, - "token": "string" - } - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_DIY_ONLINE_STATUS = "napcat_set_diy_online_status" - """设置自定义在线状态 - - Args: - face_id (Optional[str|int]): 表情ID(必需) - face_type (Optional[str|int]): 表情类型 - wording (Optional[str]):描述文本 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": "string", - "message": "string", - "wording": "string", - "echo": "string" - } - """ - - class MESSAGE(Enum): - """ - 该分类是对信息相关的操作,只能由外部触发,napcat_plugin负责处理 - """ - - SEND_PRIVATE_MSG = "napcat_send_private_msg" - """发送私聊消息 - - Args: - user_id (Optional[str|int]): 用户id(必需) - message (Optional[str]): 消息object(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "message_id": 0 - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SEND_POKE = "napcat_send_poke" - """发送戳一戳 - - Args: - group_id (Optional[str|int]): 群号 - user_id (Optional[str|int]): 对方QQ号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - DELETE_MSG = "napcat_delete_msg" - """撤回消息 - - Args: - message_id (Optional[str|int]): 消息id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_MSG_HISTORY = "napcat_get_group_msg_history" - """获取群历史消息 - - Args: - group_id (Optional[str|int]): 群号(必需) - message_seq (Optional[str|int]): 消息序号,0为最新 - count (Optional[int]): 获取数量 - reverseOrder (Optional[bool]): 是否倒序 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "messages": [ - { - "self_id": 0, - "user_id": 0, - "time": 0, - "message_id": 0, - "message_seq": 0, - "real_id": 0, - "real_seq": "string", - "message_type": "string", - "sender": { - "user_id": 0, - "nickname": "string", - "sex": "male", - "age": 0, - "card": "string", - "role": "owner" - }, - "raw_message": "string", - "font": 0, - "sub_type": "string", - "message": [ - { - "type": "text", - "data": { - "text": "string" - } - } - ], - "message_format": "string", - "post_type": "string", - "group_id": 0 - } - ] - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_MSG = "napcat_get_msg" - """获取消息详情 - - Args: - message_id (Optional[str|int]): 消息id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "self_id": 0, - "user_id": 0, - "time": 0, - "message_id": 0, - "message_seq": 0, - "real_id": 0, - "real_seq": "string", - "message_type": "string", - "sender": { - "user_id": 0, - "nickname": "string", - "sex": "male", - "age": 0, - "card": "string", - "role": "owner" - }, - "raw_message": "string", - "font": 0, - "sub_type": "string", - "message": [ - { - "type": "text", - "data": { - "text": "string" - } - } - ], - "message_format": "string", - "post_type": "string", - "group_id": 0 - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_FORWARD_MSG = "napcat_get_forward_msg" - """获取合并转发消息 - - Args: - message_id (Optional[str|int]): 消息id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "messages": [ - { - "self_id": 0, - "user_id": 0, - "time": 0, - "message_id": 0, - "message_seq": 0, - "real_id": 0, - "real_seq": "string", - "message_type": "string", - "sender": { - "user_id": 0, - "nickname": "string", - "sex": "male", - "age": 0, - "card": "string", - "role": "owner" - }, - "raw_message": "string", - "font": 0, - "sub_type": "string", - "message": [ - { - "type": "text", - "data": { - "text": "string" - } - } - ], - "message_format": "string", - "post_type": "string", - "group_id": 0 - } - ] - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_MSG_EMOJI_LIKE = "napcat_set_msg_emoji_like" - """贴表情 - - Args: - message_id (Optional[str|int]): 消息id(必需) - emoji_id (Optional[int]): 表情id(必需) - set (Optional[bool]): 是否贴(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "result": 0, - "errMsg": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_FRIEND_MSG_HISTORY = "napcat_get_friend_msg_history" - """获取好友历史消息 - - Args: - user_id (Optional[str|int]): 用户id(必需) - message_seq (Optional[str|int]): 消息序号,0为最新 - count (Optional[int]): 获取数量 - reverseOrder (Optional[bool]): 是否倒序 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "messages": [ - { - "self_id": 0, - "user_id": 0, - "time": 0, - "message_id": 0, - "message_seq": 0, - "real_id": 0, - "real_seq": "string", - "message_type": "string", - "sender": { - "user_id": 0, - "nickname": "string", - "sex": "male", - "age": 0, - "card": "string", - "role": "owner" - }, - "raw_message": "string", - "font": 0, - "sub_type": "string", - "message": [ - { - "type": "text", - "data": { - "text": "string" - } - } - ], - "message_format": "string", - "post_type": "string", - "group_id": 0 - } - ] - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - FETCH_EMOJI_LIKE = "napcat_fetch_emoji_like" - """获取贴表情详情 - - Args: - message_id (Optional[str|int]): 消息id(必需) - emojiId (Optional[str]): 表情id(必需) - emojiType (Optional[str]): 表情类型(必需) - count (Optional[int]): 返回数量 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "result": 0, - "errMsg": "string", - "emojiLikesList": [ - { - "tinyId": "string", - "nickName": "string", - "headUrl": "string" - } - ], - "cookie": "string", - "isLastPage": true, - "isFirstPage": true - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SEND_FORWARD_MSG = "napcat_send_forward_msg" - """发送合并转发消息 - - Args: - group_id (Optional[str|int]): 群号 - user_id (Optional[str|int]): 用户id - messages (Optional[dict]): 一级合并转发消息节点(必需) - news (Optional[dict]): 原转发消息之外的消息(必需) - prompt (Optional[str]): 外显(必需) - summary (Optional[str]): 底下文本(必需) - source (Optional[str]): 内容(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": {}, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SEND_GROUP_AI_RECORD = "napcat_send_group_ai_record" - """发送群AI语音 - - Args: - group_id (Optional[str|int]): 群号(必需) - character (Optional[str]): 角色id(必需) - text (Optional[str]): 文本(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "message_id": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - - class GROUP(Enum): - """ - 该分类是对群聊相关的操作,只能由外部触发,napcat_plugin负责处理 - """ - - GET_GROUP_INFO = "napcat_get_group_info" - """获取群信息 - - Args: - group_id (Optional[str|int]): 群号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "group_all_shut": 0, - "group_remark": "string", - "group_id": "string", - "group_name": "string", - "member_count": 0, - "max_member_count": 0 - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_ADD_OPTION = "napcat_set_group_add_option" - """设置群添加选项 - - Args: - group_id (Optional[str|int]): 群号(必需) - add_type (Optional[str]): 群添加类型(必需) - group_question (Optional[str]): 群添加问题 - group_answer (Optional[str]): 群添加答案 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_KICK_MEMBERS = "napcat_set_group_kick_members" - """批量踢出群成员 - - Args: - group_id (Optional[str|int]): 群号(必需) - user_id (Optional[List[str|int]]): 用户id列表(必需) - reject_add_request (Optional[bool]): 是否群拉黑 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_REMARK = "napcat_set_group_remark" - """设置群备注 - - Args: - group_id (Optional[str]): 群号(必需) - remark (Optional[str]): 备注内容(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_KICK = "napcat_set_group_kick" - """群踢人 - - Args: - group_id (Optional[str|int]): 群号(必需) - user_id (Optional[str|int]): 用户id(必需) - reject_add_request (Optional[bool]): 是否群拉黑 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_SYSTEM_MSG = "napcat_get_group_system_msg" - """获取群系统消息 - - Args: - count (Optional[int]): 获取数量(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "InvitedRequest": [ - { - "request_id": 0, - "invitor_uin": 0, - "invitor_nick": "string", - "group_id": 0, - "message": "string", - "group_name": "string", - "checked": true, - "actor": 0, - "requester_nick": "string" - } - ], - "join_requests": [ - { - "request_id": 0, - "invitor_uin": 0, - "invitor_nick": "string", - "group_id": 0, - "message": "string", - "group_name": "string", - "checked": true, - "actor": 0, - "requester_nick": "string" - } - ] - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_BAN = "napcat_set_group_ban" - """群禁言 - - Args: - group_id (Optional[str|int]): 群号(必需) - user_id (Optional[str|int]): 用户id(必需) - duration (Optional[int]): 禁言时间:秒(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_ESSENCE_MSG_LIST = "napcat_get_essence_msg_list" - """获取群精华消息 - - Args: - group_id (Optional[str|int]): 群号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - { - "msg_seq": 0, - "msg_random": 0, - "sender_id": 0, - "sender_nick": "string", - "operator_id": 0, - "operator_nick": "string", - "message_id": 0, - "operator_time": 0, - "content": [ - { - "type": "text", - "data": { - "text": "string" - } - } - ] - } - ], - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_WHOLE_BAN = "napcat_set_group_whole_ban" - """全体禁言 - - Args: - group_id (Optional[str|int]): 群号(必需) - enable (Optional[bool]): 是否启用(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_PORTRAINT = "napcat_set_group_portrait" - """设置群头像 - - Args: - group_id (Optional[str|int]): 群号(必需) - file (Optional[str]): 文件路径(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "result": 0, - "errMsg": "success" - }, - "message": "", - "wording": "", - "echo": null - } - """ - SET_GROUP_ADMIN = "napcat_set_group_admin" - """设置群管理 - - Args: - group_id (Optional[str|int]): 群号(必需) - user_id (Optional[str|int]): 用户id(必需) - enable (Optional[bool]): 是否设为群管理(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_CARD = "napcat_group_card" - """设置群成员名片 - - Args: - group_id (Optional[str|int]): 群号(必需) - user_id (Optional[str|int]): 用户id(必需) - card (Optional[str]): 为空则为取消群名片 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_ESSENCE_MSG = "napcat_set_essence_msg" - """设置群精华消息 - - Args: - message_id (Optional[str|int]): 消息id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "errCode": 0, - "errMsg": "success", - "result": { - "wording": "", - "digestUin": "0", - "digestTime": 0, - "msg": { - "groupCode": "0", - "msgSeq": 0, - "msgRandom": 0, - "msgContent": [], - "textSize": "0", - "picSize": "0", - "videoSize": "0", - "senderUin": "0", - "senderTime": 0, - "addDigestUin": "0", - "addDigestTime": 0, - "startTime": 0, - "latestMsgSeq": 0, - "opType": 0 - }, - "errorCode": 0 - } - }, - "message": "", - "wording": "", - "echo": null - } - """ - SET_GROUP_NAME = "napcat_set_group_name" - """设置群名 - - Args: - group_id (Optional[str|int]): 群号(必需) - group_name (Optional[str]): 群名(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - DELETE_ESSENCE_MSG = "napcat_delete_essence_msg" - """删除群精华消息 - - Args: - message_id (Optional[str|int]): 消息id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict:{ - "status": "ok", - "retcode": 0, - "data": { - "errCode": 0, - "errMsg": "success", - "result": { - "wording": "", - "digestUin": "0", - "digestTime": 0, - "msg": { - "groupCode": "0", - "msgSeq": 0, - "msgRandom": 0, - "msgContent": [], - "textSize": "0", - "picSize": "0", - "videoSize": "0", - "senderUin": "0", - "senderTime": 0, - "addDigestUin": "0", - "addDigestTime": 0, - "startTime": 0, - "latestMsgSeq": 0, - "opType": 0 - }, - "errorCode": 0 - } - }, - "message": "", - "wording": "", - "echo": null - } - """ - SET_GROUP_LEAVE = "napcat_set_group_leave" - """退群 - - Args: - group_id (Optional[str|int]): 群号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SEND_GROUP_NOTICE = "napcat_group_notice" - """发送群公告 - - Args: - group_id (Optional[str|int]): 群号(必需) - content (Optional[str]): 公告内容(必需) - image (Optional[str]): 图片地址 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_SPECIAL_TITLE = "napcat_set_group_special_title" - """设置群头衔 - - Args: - group_id (Optional[str|int]): 群号(必需) - user_id (Optional[str|int]): 用户id(必需) - special_title (Optional[str]): 为空则取消头衔 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_NOTICE = "napcat_get_group_notice" - """获取群公告 - - Args: - group_id (Optional[str|int]): 群号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - { - "notice_id": "63491e2f000000004f4d1e677d2b0200", - "sender_id": 123, - "publish_time": 1730039119, - "message": { - "text": "这是一条神奇的群公告", - "image": [ - { - "id": "aJJBbZ6BqyLiaC1kmpvIWGBBkJerEfpRBHX5Brxbaurs", - "height": "400", - "width": "400" - } - ] - } - } - ], - "message": "", - "wording": "", - "echo": null - } - """ - SET_GROUP_ADD_REQUEST = "napcat_set_group_add_request" - """处理加群请求 - - Args: - flag (Optional[str]): 请求id(必需) - approve (Optional[bool]): 是否同意(必需) - reason (Optional[str]): 拒绝理由 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": null, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_LIST = "napcat_get_group_list" - """获取群列表 - - Args: - no_cache (Optional[bool]): 是否不缓存 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - { - "group_all_shut": 0, - "group_remark": "string", - "group_id": "string", - "group_name": "string", - "member_count": 0, - "max_member_count": 0 - } - ], - "message": "string", - "wording": "string", - "echo": "string" - } - """ - DELETE_GROUP_NOTICE = "napcat_del_group_notice" - """删除群公告 - - Args: - group_id (Optional[str|int]): 群号(必需) - notice_id (Optional[str]): 公告id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "result": 0, - "errMsg": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_MEMBER_INFO = "napcat_get_group_member_info" - """获取群成员信息 - - Args: - group_id (Optional[str|int]): 群号(必需) - user_id (Optional[str|int]): 用户id(必需) - no_cache (Optional[bool]): 是否不缓存 - raw (Optional[dict]): 原始请求体 - - Returns: - dict:{ - "status": "ok", - "retcode": 0, - "data": { - "group_id": 0, - "user_id": 0, - "nickname": "string", - "card": "string", - "sex": "string", - "age": 0, - "join_time": 0, - "last_sent_time": 0, - "level": 0, - "qq_level": 0, - "role": "string", - "title": "string", - "area": "string", - "unfriendly": true, - "title_expire_time": 0, - "card_changeable": true, - "shut_up_timestamp": 0, - "is_robot": true, - "qage": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_MEMBER_LIST = "napcat_get_group_member_list" - """获取群成员列表 - - Args: - group_id (Optional[str|int]): 群号(必需) - no_cache (Optional[bool]): 是否不缓存 - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - { - "group_id": 0, - "user_id": 0, - "nickname": "string", - "card": "string", - "sex": "string", - "age": 0, - "join_time": 0, - "last_sent_time": 0, - "level": 0, - "qq_level": 0, - "role": "string", - "title": "string", - "area": "string", - "unfriendly": true, - "title_expire_time": 0, - "card_changeable": true, - "shut_up_timestamp": 0, - "is_robot": true, - "qage": "string" - } - ], - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_HONOR_INFO = "napcat_get_group_honor_info" - """获取群荣誉 - - Args: - group_id (Optional[str|int]): 群号(必需) - type (Optional[str]): 看详情 - raw (Optional[dict]): 原始请求体 - - Returns: - dict:{ - "status": "ok", - "retcode": 0, - "data": { - "group_id": "string", - "current_talkative": { - "user_id": 0, - "nickname": "string", - "avatar": 0, - "description": "string" - }, - "talkative_list": [ - { - "user_id": 0, - "nickname": "string", - "avatar": 0, - "description": "string" - } - ], - "performer_list": [ - { - "user_id": 0, - "nickname": "string", - "avatar": 0, - "description": "string" - } - ], - "legend_list": [ - { - "user_id": 0, - "nickname": "string", - "avatar": 0, - "description": "string" - } - ], - "emotion_list": [ - { - "user_id": 0, - "nickname": "string", - "avatar": 0, - "description": "string" - } - ], - "strong_newbie_list": [ - { - "user_id": 0, - "nickname": "string", - "avatar": 0, - "description": "string" - } - ] - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_INFO_EX = "napcat_get_group_info_ex" - """获取群信息ex - - Args: - group_id (Optional[str|int]): 群号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "groupCode": "790514019", - "resultCode": 0, - "extInfo": { - "groupInfoExtSeq": 1, - "reserve": 0, - "luckyWordId": "0", - "lightCharNum": 0, - "luckyWord": "", - "starId": 0, - "essentialMsgSwitch": 0, - "todoSeq": 0, - "blacklistExpireTime": 0, - "isLimitGroupRtc": 0, - "companyId": 0, - "hasGroupCustomPortrait": 1, - "bindGuildId": "0", - "groupOwnerId": { - "memberUin": "1129317309", - "memberUid": "u_4_QA-QaFryh-Ocgsv4_8EQ", - "memberQid": "" - }, - "essentialMsgPrivilege": 0, - "msgEventSeq": "0", - "inviteRobotSwitch": 0, - "gangUpId": "0", - "qqMusicMedalSwitch": 0, - "showPlayTogetherSwitch": 0, - "groupFlagPro1": "0", - "groupBindGuildIds": { - "guildIds": [] - }, - "viewedMsgDisappearTime": "0", - "groupExtFlameData": { - "switchState": 0, - "state": 0, - "dayNums": [], - "version": 0, - "updateTime": "0", - "isDisplayDayNum": false - }, - "groupBindGuildSwitch": 0, - "groupAioBindGuildId": "0", - "groupExcludeGuildIds": { - "guildIds": [] - }, - "fullGroupExpansionSwitch": 0, - "fullGroupExpansionSeq": "0", - "inviteRobotMemberSwitch": 0, - "inviteRobotMemberExamine": 0, - "groupSquareSwitch": 0 - } - }, - "message": "", - "wording": "", - "echo": null - } - """ - GET_GROUP_AT_ALL_REMAIN = "napcat_get_group_at_all_remain" - """获取群 @全体成员 剩余次数 - - Args: - group_id (Optional[str|int]): 群号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "can_at_all": true, - "remain_at_all_count_for_group": 0, - "remain_at_all_count_for_uin": 0 - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_SHUT_LIST = "napcat_get_group_shut_list" - """获取群禁言列表 - - Args: - group_id (Optional[str|int]): 群号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": [ - { - "uid": "string", - "qid": "string", - "uin": "string", - "nick": "string", - "remark": "string", - "cardType": 0, - "cardName": "string", - "role": 0, - "avatarPath": "string", - "shutUpTime": 0, - "isDelete": true, - "isSpecialConcerned": true, - "isSpecialShield": true, - "isRobot": true, - "groupHonor": {}, - "memberRealLevel": 0, - "memberLevel": 0, - "globalGroupLevel": 0, - "globalGroupPoint": 0, - "memberTitleId": 0, - "memberSpecialTitle": "string", - "specialTitleExpireTime": "string", - "userShowFlag": 0, - "userShowFlagNew": 0, - "richFlag": 0, - "mssVipType": 0, - "bigClubLevel": 0, - "bigClubFlag": 0, - "autoRemark": "string", - "creditLevel": 0, - "joinTime": 0, - "lastSpeakTime": 0, - "memberFlag": 0, - "memberFlagExt": 0, - "memberMobileFlag": 0, - "memberFlagExt2": 0, - "isSpecialShielded": true, - "cardNameId": 0 - } - ], - "message": "string", - "wording": "string", - "echo": "string" - } - """ - GET_GROUP_IGNORED_NOTIFIES = "napcat_get_group_ignored_notifies" - """获取群过滤系统消息 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "InvitedRequest": [ - { - "request_id": 0, - "invitor_uin": 0, - "invitor_nick": "string", - "group_id": 0, - "message": "string", - "group_name": "string", - "checked": true, - "actor": 0, - "requester_nick": "string" - } - ], - "join_requests": [ - { - "request_id": 0, - "invitor_uin": 0, - "invitor_nick": "string", - "group_id": 0, - "message": "string", - "group_name": "string", - "checked": true, - "actor": 0, - "requester_nick": "string" - } - ] - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ - SET_GROUP_SIGN = "napcat_set_group_sign" - """群打卡 - - Args: - group_id (Optional[str|int]): 群号(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: {} - """ - - class FILE(Enum): ... - - class PERSONAL(Enum): - SET_INPUT_STATUS = "napcat_set_input_status" - """ - 设置输入状态 - - Args: - user_id (Optional[str|int]): 用户id(必需) - event_type (Optional[int]): 输入状态id(必需) - raw (Optional[dict]): 原始请求体 - - Returns: - dict: { - "status": "ok", - "retcode": 0, - "data": { - "result": 0, - "errMsg": "string" - }, - "message": "string", - "wording": "string", - "echo": "string" - } - """ diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py deleted file mode 100644 index 2437f0162..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ /dev/null @@ -1,430 +0,0 @@ -import asyncio -import inspect -import orjson -from typing import ClassVar, List - -import websockets as Server - -from src.common.logger import get_logger -from src.plugin_system import BaseEventHandler, BasePlugin, ConfigField, EventType, register_plugin -from src.plugin_system.apis import config_api -from src.plugin_system.core.event_manager import event_manager - -from . import CONSTS, event_handlers, event_types -from .src.message_chunker import chunker, reassembler -from .src.mmc_com_layer import mmc_start_com, mmc_stop_com, router -from .src.recv_handler.message_handler import message_handler -from .src.recv_handler.message_sending import message_send_instance -from .src.recv_handler.meta_event_handler import meta_event_handler -from .src.recv_handler.notice_handler import notice_handler -from .src.response_pool import check_timeout_response, put_response -from .src.send_handler import send_handler -from .src.stream_router import stream_router -from .src.websocket_manager import websocket_manager - -logger = get_logger("napcat_adapter") - -# 旧的全局消息队列已被流路由器替代 -# message_queue = asyncio.Queue() - - -def get_classes_in_module(module): - classes = [] - for _name, member in inspect.getmembers(module): - if inspect.isclass(member): - classes.append(member) - return classes - - -async def message_recv(server_connection: Server.ServerConnection): - await message_handler.set_server_connection(server_connection) - asyncio.create_task(notice_handler.set_server_connection(server_connection)) - await send_handler.set_server_connection(server_connection) - async for raw_message in server_connection: - # 只在debug模式下记录原始消息 - if logger.level <= 10: # DEBUG level - logger.debug(f"{raw_message[:1500]}..." if (len(raw_message) > 1500) else raw_message) - decoded_raw_message: dict = orjson.loads(raw_message) - try: - # 首先尝试解析原始消息 - decoded_raw_message: dict = orjson.loads(raw_message) - - # 检查是否是切片消息 (来自 MMC) - if chunker.is_chunk_message(decoded_raw_message): - logger.debug("接收到切片消息,尝试重组") - # 尝试重组消息 - reassembled_message = await reassembler.add_chunk(decoded_raw_message) - if reassembled_message: - # 重组完成,处理完整消息 - logger.debug("消息重组完成,处理完整消息") - decoded_raw_message = reassembled_message - else: - # 切片尚未完整,继续等待更多切片 - logger.debug("等待更多切片...") - continue - - # 处理完整消息(可能是重组后的,也可能是原本就完整的) - post_type = decoded_raw_message.get("post_type") - if post_type in ["meta_event", "message", "notice"]: - # 使用流路由器路由消息到对应的聊天流 - await stream_router.route_message(decoded_raw_message) - elif post_type is None: - await put_response(decoded_raw_message) - - except orjson.JSONDecodeError as e: - logger.error(f"消息解析失败: {e}") - logger.debug(f"原始消息: {raw_message[:500]}...") - except Exception as e: - logger.error(f"处理消息时出错: {e}") - logger.debug(f"原始消息: {raw_message[:500]}...") - - -# 旧的单消费者消息处理循环已被流路由器替代 -# 现在每个聊天流都有自己的消费者协程 -# async def message_process(): -# """消息处理主循环""" -# ... - - -async def napcat_server(plugin_config: dict): - """启动 Napcat WebSocket 连接(支持正向和反向连接)""" - # 使用插件系统配置API获取配置 - mode = config_api.get_plugin_config(plugin_config, "napcat_server.mode") - logger.info(f"正在启动 adapter,连接模式: {mode}") - - try: - await websocket_manager.start_connection(message_recv, plugin_config) - except Exception as e: - logger.error(f"启动 WebSocket 连接失败: {e}") - raise - - -async def graceful_shutdown(): - """优雅关闭所有组件""" - try: - logger.info("正在关闭adapter...") - - # 停止流路由器 - try: - await stream_router.stop() - except Exception as e: - logger.warning(f"停止流路由器时出错: {e}") - - # 停止消息重组器的清理任务 - try: - await reassembler.stop_cleanup_task() - except Exception as e: - logger.warning(f"停止消息重组器清理任务时出错: {e}") - - # 停止功能管理器文件监控(已迁移到插件系统配置,无需操作) - - # 关闭消息处理器(包括消息缓冲器) - try: - await message_handler.shutdown() - except Exception as e: - logger.warning(f"关闭消息处理器时出错: {e}") - - # 关闭 WebSocket 连接 - try: - await websocket_manager.stop_connection() - except Exception as e: - logger.warning(f"关闭WebSocket连接时出错: {e}") - - # 关闭 MoFox-Bot 连接 - try: - await mmc_stop_com() - except Exception as e: - logger.warning(f"关闭MoFox-Bot连接时出错: {e}") - - # 取消所有剩余任务 - current_task = asyncio.current_task() - tasks = [t for t in asyncio.all_tasks() if t is not current_task and not t.done()] - - if tasks: - logger.info(f"正在取消 {len(tasks)} 个剩余任务...") - for task in tasks: - task.cancel() - - # 等待任务取消完成,忽略 CancelledError - try: - await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=True), timeout=10) - except asyncio.TimeoutError: - logger.warning("部分任务取消超时") - except Exception as e: - logger.debug(f"任务取消过程中的异常(可忽略): {e}") - - logger.info("Adapter已成功关闭") - - except Exception as e: - logger.error(f"Adapter关闭中出现错误: {e}") - - -class LauchNapcatAdapterHandler(BaseEventHandler): - """自动启动Adapter""" - - handler_name: str = "launch_napcat_adapter_handler" - handler_description: str = "自动启动napcat adapter" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [EventType.ON_START] - - async def execute(self, kwargs): - # 启动消息重组器的清理任务 - logger.info("启动消息重组器...") - await reassembler.start_cleanup_task() - - # 启动流路由器 - logger.info("启动流路由器...") - await stream_router.start() - - logger.info("开始启动Napcat Adapter") - - # 创建单独的异步任务,防止阻塞主线程 - asyncio.create_task(self._start_maibot_connection()) - asyncio.create_task(napcat_server(self.plugin_config)) - # 不再需要 message_process 任务,由流路由器管理消费者 - asyncio.create_task(check_timeout_response()) - - async def _start_maibot_connection(self): - """非阻塞方式启动MoFox-Bot连接,等待主服务启动后再连接""" - # 等待一段时间让MoFox-Bot主服务完全启动 - await asyncio.sleep(5) - - max_attempts = 10 - attempt = 0 - - while attempt < max_attempts: - try: - logger.info(f"尝试连接MoFox-Bot (第{attempt + 1}次)") - await mmc_start_com(self.plugin_config) - message_send_instance.maibot_router = router - logger.info("MoFox-Bot router连接已建立") - return - except Exception as e: - attempt += 1 - if attempt >= max_attempts: - logger.error(f"MoFox-Bot连接失败,已达到最大重试次数: {e}") - return - else: - delay = min(2 + attempt, 10) # 逐渐增加延迟,最大10秒 - logger.warning(f"MoFox-Bot连接失败: {e},{delay}秒后重试") - await asyncio.sleep(delay) - - -class StopNapcatAdapterHandler(BaseEventHandler): - """关闭Adapter""" - - handler_name: str = "stop_napcat_adapter_handler" - handler_description: str = "关闭napcat adapter" - weight: int = 100 - intercept_message: bool = False - init_subscribe: ClassVar[list] = [EventType.ON_STOP] - - async def execute(self, kwargs): - await graceful_shutdown() - return - - -@register_plugin -class NapcatAdapterPlugin(BasePlugin): - plugin_name = CONSTS.PLUGIN_NAME - dependencies: ClassVar[List[str]] = [] # 插件依赖列表 - python_dependencies: ClassVar[List[str]] = [] # Python包依赖列表 - config_file_name: str = "config.toml" # 配置文件名 - - @property - def enable_plugin(self) -> bool: - """通过配置文件动态控制插件启用状态""" - # 如果已经通过配置加载了状态,使用配置中的值 - # 否则使用默认值(禁用状态) - return False - - # 配置节描述 - config_section_descriptions: ClassVar[dict] = {"plugin": "插件基本信息"} - - # 配置Schema定义 - config_schema: ClassVar[dict] = { - "plugin": { - "name": ConfigField(type=str, default="napcat_adapter_plugin", description="插件名称"), - "version": ConfigField(type=str, default="1.1.0", description="插件版本"), - "config_version": ConfigField(type=str, default="1.3.1", description="配置文件版本"), - "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), - }, - "inner": { - "version": ConfigField(type=str, default="0.2.1", description="配置版本号,请勿修改"), - }, - "nickname": { - "nickname": ConfigField(type=str, default="", description="昵称配置(目前未使用)"), - }, - "napcat_server": { - "mode": ConfigField( - type=str, - default="reverse", - description="连接模式:reverse=反向连接(作为服务器), forward=正向连接(作为客户端)", - choices=["reverse", "forward"], - ), - "host": ConfigField(type=str, default="localhost", description="主机地址"), - "port": ConfigField(type=int, default=8095, description="端口号"), - "url": ConfigField( - type=str, - default="", - description="正向连接时的完整WebSocket URL,如 ws://localhost:8080/ws (仅在forward模式下使用)", - ), - "access_token": ConfigField( - type=str, default="", description="WebSocket 连接的访问令牌,用于身份验证(可选)" - ), - "heartbeat_interval": ConfigField(type=int, default=30, description="心跳间隔时间(按秒计)"), - }, - "maibot_server": { - "platform_name": ConfigField(type=str, default="qq", description="平台名称,用于消息路由"), - "host": ConfigField(type=str, default="", description="MoFox-Bot服务器地址,留空则使用全局配置"), - "port": ConfigField(type=int, default=0, description="MoFox-Bot服务器端口,设为0则使用全局配置"), - }, - "voice": { - "use_tts": ConfigField( - type=bool, default=False, description="是否使用tts语音(请确保你配置了tts并有对应的adapter)" - ), - }, - "slicing": { - "max_frame_size": ConfigField( - type=int, default=64, description="WebSocket帧的最大大小,单位为字节,默认64KB" - ), - "delay_ms": ConfigField(type=int, default=10, description="切片发送间隔时间,单位为毫秒"), - }, - "debug": { - "level": ConfigField( - type=str, - default="INFO", - description="日志等级(DEBUG, INFO, WARNING, ERROR, CRITICAL)", - choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], - ), - }, - "stream_router": { - "max_streams": ConfigField(type=int, default=500, description="最大并发流数量"), - "stream_timeout": ConfigField(type=int, default=600, description="流不活跃超时时间(秒),超时后自动清理"), - "stream_queue_size": ConfigField(type=int, default=100, description="每个流的消息队列大小"), - "cleanup_interval": ConfigField(type=int, default=60, description="清理不活跃流的间隔时间(秒)"), - }, - "features": { - # 权限设置 - "group_list_type": ConfigField( - type=str, - default="blacklist", - description="群聊列表类型:whitelist(白名单)或 blacklist(黑名单)", - choices=["whitelist", "blacklist"], - ), - "group_list": ConfigField(type=list, default=[], description="群聊ID列表"), - "private_list_type": ConfigField( - type=str, - default="blacklist", - description="私聊列表类型:whitelist(白名单)或 blacklist(黑名单)", - choices=["whitelist", "blacklist"], - ), - "private_list": ConfigField(type=list, default=[], description="用户ID列表"), - "ban_user_id": ConfigField( - type=list, default=[], description="全局禁止用户ID列表,这些用户无法在任何地方使用机器人" - ), - "ban_qq_bot": ConfigField(type=bool, default=False, description="是否屏蔽QQ官方机器人消息"), - # 聊天功能设置 - "enable_poke": ConfigField(type=bool, default=True, description="是否启用戳一戳功能"), - "ignore_non_self_poke": ConfigField(type=bool, default=False, description="是否无视不是针对自己的戳一戳"), - "poke_debounce_seconds": ConfigField( - type=int, default=3, description="戳一戳防抖时间(秒),在指定时间内第二次针对机器人的戳一戳将被忽略" - ), - "enable_reply_at": ConfigField(type=bool, default=True, description="是否启用引用回复时艾特用户的功能"), - "reply_at_rate": ConfigField(type=float, default=0.5, description="引用回复时艾特用户的几率 (0.0 ~ 1.0)"), - "enable_emoji_like": ConfigField(type=bool, default=True, description="是否启用群聊表情回复功能"), - # 视频处理设置 - "enable_video_analysis": ConfigField(type=bool, default=True, description="是否启用视频识别功能"), - "max_video_size_mb": ConfigField(type=int, default=100, description="视频文件最大大小限制(MB)"), - "download_timeout": ConfigField(type=int, default=60, description="视频下载超时时间(秒)"), - "supported_formats": ConfigField( - type=list, default=["mp4", "avi", "mov", "mkv", "flv", "wmv", "webm"], description="支持的视频格式" - ), - }, - } - - # 配置节描述 - config_section_descriptions: ClassVar[dict] = { - "plugin": "插件基本信息", - "inner": "内部配置信息(请勿修改)", - "nickname": "昵称配置(目前未使用)", - "napcat_server": "Napcat连接的ws服务设置", - "maibot_server": "连接麦麦的ws服务设置", - "voice": "发送语音设置", - "slicing": "WebSocket消息切片设置", - "debug": "调试设置", - "stream_router": "流路由器设置(按聊天流分配消费者,提升高并发性能)", - "features": "功能设置(权限控制、聊天功能、视频处理等)", - } - - def register_events(self): - # 注册事件 - for e in event_types.NapcatEvent.ON_RECEIVED: - event_manager.register_event(e, allowed_triggers=[self.plugin_name]) - - for e in event_types.NapcatEvent.ACCOUNT: - event_manager.register_event(e, allowed_subscribers=[f"{e.value}_handler"]) - - for e in event_types.NapcatEvent.GROUP: - event_manager.register_event(e, allowed_subscribers=[f"{e.value}_handler"]) - - for e in event_types.NapcatEvent.MESSAGE: - event_manager.register_event(e, allowed_subscribers=[f"{e.value}_handler"]) - - def get_plugin_components(self): - self.register_events() - - components = [] - components.append((LauchNapcatAdapterHandler.get_handler_info(), LauchNapcatAdapterHandler)) - components.append((StopNapcatAdapterHandler.get_handler_info(), StopNapcatAdapterHandler)) - components.extend( - (handler.get_handler_info(), handler) - for handler in get_classes_in_module(event_handlers) - if issubclass(handler, BaseEventHandler) - ) - return components - - async def on_plugin_loaded(self): - # 初始化数据库表 - await self._init_database_tables() - - # 设置插件配置 - message_send_instance.set_plugin_config(self.config) - # 设置chunker的插件配置 - chunker.set_plugin_config(self.config) - # 设置response_pool的插件配置 - from .src.response_pool import set_plugin_config as set_response_pool_config - - set_response_pool_config(self.config) - # 设置send_handler的插件配置 - send_handler.set_plugin_config(self.config) - # 设置message_handler的插件配置 - message_handler.set_plugin_config(self.config) - # 设置notice_handler的插件配置 - notice_handler.set_plugin_config(self.config) - # 设置meta_event_handler的插件配置 - meta_event_handler.set_plugin_config(self.config) - - # 设置流路由器的配置 - stream_router.max_streams = config_api.get_plugin_config(self.config, "stream_router.max_streams", 500) - stream_router.stream_timeout = config_api.get_plugin_config(self.config, "stream_router.stream_timeout", 600) - stream_router.stream_queue_size = config_api.get_plugin_config(self.config, "stream_router.stream_queue_size", 100) - stream_router.cleanup_interval = config_api.get_plugin_config(self.config, "stream_router.cleanup_interval", 60) - - # 设置其他handler的插件配置(现在由component_registry在注册时自动设置) - - async def _init_database_tables(self): - """初始化插件所需的数据库表""" - try: - from src.common.database.core.engine import get_engine - from .src.database import NapcatBanRecord - - engine = await get_engine() - async with engine.begin() as conn: - # 创建 napcat_ban_records 表 - await conn.run_sync(NapcatBanRecord.metadata.create_all) - - logger.info("Napcat 插件数据库表初始化成功") - except Exception as e: - logger.error(f"Napcat 插件数据库表初始化失败: {e}", exc_info=True) diff --git a/src/plugins/built_in/napcat_adapter_plugin/pyproject.toml b/src/plugins/built_in/napcat_adapter_plugin/pyproject.toml deleted file mode 100644 index a45fee969..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/pyproject.toml +++ /dev/null @@ -1,47 +0,0 @@ -[project] -name = "MoFoxBotNapcatAdapter" -version = "0.4.8" -description = "A MoFox-Bot adapter for Napcat" -dependencies = [ - "ruff>=0.12.9", -] - -[tool.ruff] - -include = ["*.py"] - -# 行长度设置 -line-length = 120 - -[tool.ruff.lint] -fixable = ["ALL"] -unfixable = [] - -# 启用的规则 -select = [ - "E", # pycodestyle 错误 - "F", # pyflakes - "B", # flake8-bugbear -] - -ignore = ["E711","E501"] - -[tool.ruff.format] -docstring-code-format = true -indent-style = "space" - - -# 使用双引号表示字符串 -quote-style = "double" - -# 尊重魔法尾随逗号 -# 例如: -# items = [ -# "apple", -# "banana", -# "cherry", -# ] -skip-magic-trailing-comma = false - -# 自动检测合适的换行符 -line-ending = "auto" diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/src/__init__.py deleted file mode 100644 index aecb7d6c6..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -from enum import Enum - -import tomlkit - -from src.common.logger import get_logger - -logger = get_logger("napcat_adapter") - - -class CommandType(Enum): - """命令类型""" - - GROUP_BAN = "set_group_ban" # 禁言用户 - GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言 - GROUP_KICK = "set_group_kick" # 踢出群聊 - SEND_POKE = "send_poke" # 戳一戳 - DELETE_MSG = "delete_msg" # 撤回消息 - AI_VOICE_SEND = "send_group_ai_record" # 发送群AI语音 - SET_EMOJI_LIKE = "set_msg_emoji_like" # 设置表情回应 - SEND_AT_MESSAGE = "send_at_message" # 艾特用户并发送消息 - SEND_LIKE = "send_like" # 点赞 - - def __str__(self) -> str: - return self.value - - -pyproject_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "pyproject.toml") -toml_data = tomlkit.parse(open(pyproject_path, "r", encoding="utf-8").read()) -project_data = toml_data.get("project", {}) -version = project_data.get("version", "unknown") -logger.info(f"版本\n\nMoFox-Bot-Napcat-Adapter 版本: {version}\n喜欢的话点个star喵~\n") diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/database.py b/src/plugins/built_in/napcat_adapter_plugin/src/database.py deleted file mode 100644 index d3cc7e116..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/database.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Napcat Adapter 插件数据库层 (基于主程序异步SQLAlchemy API) - -本模块替换原先的 sqlmodel + 同步Session 实现: -1. 复用主项目的异步数据库连接与迁移体系 -2. 提供与旧接口名兼容的方法(update_ban_record/create_ban_record/delete_ban_record) -3. 新增首选异步方法: update_ban_records / create_or_update / delete_record / get_ban_records - -数据语义: - user_id == 0 表示群全体禁言 - -注意: 所有方法均为异步, 需要在 async 上下文中调用。 -""" -from __future__ import annotations - -from dataclasses import dataclass -from typing import List, Optional, Sequence - -from sqlalchemy import BigInteger, Column, Index, Integer, UniqueConstraint, select -from sqlalchemy.ext.asyncio import AsyncSession - -from src.common.database.core.models import Base -from src.common.database.core import get_db_session -from src.common.logger import get_logger - -logger = get_logger("napcat_adapter") - - -class NapcatBanRecord(Base): - __tablename__ = "napcat_ban_records" - - id = Column(Integer, primary_key=True, autoincrement=True) - group_id = Column(BigInteger, nullable=False, index=True) - user_id = Column(BigInteger, nullable=False, index=True) # 0 == 全体禁言 - lift_time = Column(BigInteger, nullable=True) # -1 / None 表示未知/永久 - - __table_args__ = ( - UniqueConstraint("group_id", "user_id", name="uq_napcat_group_user"), - Index("idx_napcat_ban_group", "group_id"), - Index("idx_napcat_ban_user", "user_id"), - ) - - -@dataclass -class BanUser: - user_id: int - group_id: int - lift_time: Optional[int] = -1 - - def identity(self) -> tuple[int, int]: - return self.group_id, self.user_id - - -class NapcatDatabase: - async def _fetch_all(self, session: AsyncSession) -> Sequence[NapcatBanRecord]: - result = await session.execute(select(NapcatBanRecord)) - return result.scalars().all() - - async def get_ban_records(self) -> List[BanUser]: - async with get_db_session() as session: - rows = await self._fetch_all(session) - return [BanUser(group_id=r.group_id, user_id=r.user_id, lift_time=r.lift_time) for r in rows] - - async def update_ban_records(self, ban_list: List[BanUser]) -> None: - target_map = {b.identity(): b for b in ban_list} - async with get_db_session() as session: - rows = await self._fetch_all(session) - existing_map = {(r.group_id, r.user_id): r for r in rows} - - changed = 0 - for ident, ban in target_map.items(): - if ident in existing_map: - row = existing_map[ident] - if row.lift_time != ban.lift_time: - row.lift_time = ban.lift_time - changed += 1 - else: - session.add( - NapcatBanRecord(group_id=ban.group_id, user_id=ban.user_id, lift_time=ban.lift_time) - ) - changed += 1 - - removed = 0 - for ident, row in existing_map.items(): - if ident not in target_map: - await session.delete(row) - removed += 1 - - logger.debug( - f"Napcat ban list sync => total_incoming={len(ban_list)} created_or_updated={changed} removed={removed}" - ) - - async def create_or_update(self, ban_record: BanUser) -> None: - async with get_db_session() as session: - stmt = select(NapcatBanRecord).where( - NapcatBanRecord.group_id == ban_record.group_id, - NapcatBanRecord.user_id == ban_record.user_id, - ) - result = await session.execute(stmt) - row = result.scalars().first() - if row: - if row.lift_time != ban_record.lift_time: - row.lift_time = ban_record.lift_time - logger.debug( - f"更新禁言记录 group={ban_record.group_id} user={ban_record.user_id} lift={ban_record.lift_time}" - ) - else: - session.add( - NapcatBanRecord( - group_id=ban_record.group_id, user_id=ban_record.user_id, lift_time=ban_record.lift_time - ) - ) - logger.debug( - f"创建禁言记录 group={ban_record.group_id} user={ban_record.user_id} lift={ban_record.lift_time}" - ) - - async def delete_record(self, ban_record: BanUser) -> None: - async with get_db_session() as session: - stmt = select(NapcatBanRecord).where( - NapcatBanRecord.group_id == ban_record.group_id, - NapcatBanRecord.user_id == ban_record.user_id, - ) - result = await session.execute(stmt) - row = result.scalars().first() - if row: - await session.delete(row) - logger.debug( - f"删除禁言记录 group={ban_record.group_id} user={ban_record.user_id} lift={row.lift_time}" - ) - else: - logger.info( - f"未找到禁言记录 group={ban_record.group_id} user={ban_record.user_id}" - ) - - # 兼容旧命名 - async def update_ban_record(self, ban_list: List[BanUser]) -> None: # old name - await self.update_ban_records(ban_list) - - async def create_ban_record(self, ban_record: BanUser) -> None: # old name - await self.create_or_update(ban_record) - - async def delete_ban_record(self, ban_record: BanUser) -> None: # old name - await self.delete_record(ban_record) - - -napcat_db = NapcatDatabase() - - -def is_identical(a: BanUser, b: BanUser) -> bool: - return a.group_id == b.group_id and a.user_id == b.user_id - - -__all__ = [ - "BanUser", - "NapcatBanRecord", - "napcat_db", - "is_identical", -] \ No newline at end of file diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/message_chunker.py b/src/plugins/built_in/napcat_adapter_plugin/src/message_chunker.py deleted file mode 100644 index 86902354f..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/message_chunker.py +++ /dev/null @@ -1,280 +0,0 @@ -""" -消息切片处理模块 -用于在 Ada 发送给 MMC 时进行消息切片,利用 WebSocket 协议的自动重组特性 -仅在 Ada -> MMC 方向进行切片,其他方向(MMC -> Ada,Ada <-> Napcat)不切片 -""" - -import asyncio -import orjson -import time -import uuid -from typing import Any, Dict, List, Optional, Union - -from src.common.logger import get_logger -from src.plugin_system.apis import config_api - -logger = get_logger("napcat_adapter") - - -class MessageChunker: - """消息切片器,用于处理大消息的分片发送""" - - def __init__(self): - self.max_chunk_size = 64 * 1024 # 默认值,将在设置配置时更新 - self.plugin_config = None - - def set_plugin_config(self, plugin_config: dict): - """设置插件配置""" - self.plugin_config = plugin_config - if plugin_config: - max_frame_size = config_api.get_plugin_config(plugin_config, "slicing.max_frame_size", 64) - self.max_chunk_size = max_frame_size * 1024 - - def should_chunk_message(self, message: Union[str, Dict[str, Any]]) -> bool: - """判断消息是否需要切片""" - try: - if isinstance(message, dict): - message_str = orjson.dumps(message, option=orjson.OPT_NON_STR_KEYS).decode('utf-8') - else: - message_str = message - return len(message_str.encode("utf-8")) > self.max_chunk_size - except Exception as e: - logger.error(f"检查消息大小时出错: {e}") - return False - - def chunk_message( - self, message: Union[str, Dict[str, Any]], chunk_id: Optional[str] = None - ) -> List[Dict[str, Any]]: - """ - 将消息切片 - - Args: - message: 要切片的消息(字符串或字典) - chunk_id: 切片组ID,如果不提供则自动生成 - - Returns: - 切片后的消息字典列表 - """ - try: - # 统一转换为字符串 - if isinstance(message, dict): - message_str = orjson.dumps(message, option=orjson.OPT_NON_STR_KEYS).decode('utf-8') - else: - message_str = message - - if not self.should_chunk_message(message_str): - # 不需要切片的情况,如果输入是字典则返回字典,如果是字符串则包装成非切片标记的字典 - if isinstance(message, dict): - return [message] - else: - return [{"_original_message": message_str}] - - if chunk_id is None: - chunk_id = str(uuid.uuid4()) - - message_bytes = message_str.encode("utf-8") - total_size = len(message_bytes) - - # 计算需要多少个切片 - num_chunks = (total_size + self.max_chunk_size - 1) // self.max_chunk_size - - chunks = [] - for i in range(num_chunks): - start_pos = i * self.max_chunk_size - end_pos = min(start_pos + self.max_chunk_size, total_size) - - chunk_data = message_bytes[start_pos:end_pos] - - # 构建切片消息 - chunk_message = { - "__mmc_chunk_info__": { - "chunk_id": chunk_id, - "chunk_index": i, - "total_chunks": num_chunks, - "chunk_size": len(chunk_data), - "total_size": total_size, - "timestamp": time.time(), - }, - "__mmc_chunk_data__": chunk_data.decode("utf-8", errors="ignore"), - "__mmc_is_chunked__": True, - } - - chunks.append(chunk_message) - - logger.debug(f"消息切片完成: {total_size} bytes -> {num_chunks} chunks (ID: {chunk_id})") - return chunks - - except Exception as e: - logger.error(f"消息切片时出错: {e}") - # 出错时返回原消息 - if isinstance(message, dict): - return [message] - else: - return [{"_original_message": message}] - - def is_chunk_message(self, message: Union[str, Dict[str, Any]]) -> bool: - """判断是否是切片消息""" - try: - if isinstance(message, str): - data = orjson.loads(message) - else: - data = message - - return ( - isinstance(data, dict) - and "__mmc_chunk_info__" in data - and "__mmc_chunk_data__" in data - and "__mmc_is_chunked__" in data - ) - except (orjson.JSONDecodeError, TypeError): - return False - - -class MessageReassembler: - """消息重组器,用于重组接收到的切片消息""" - - def __init__(self, timeout: int = 30): - self.timeout = timeout - self.chunk_buffers: Dict[str, Dict[str, Any]] = {} - self._cleanup_task = None - - async def start_cleanup_task(self): - """启动清理任务""" - if self._cleanup_task is None: - self._cleanup_task = asyncio.create_task(self._cleanup_expired_chunks()) - - async def stop_cleanup_task(self): - """停止清理任务""" - if self._cleanup_task: - self._cleanup_task.cancel() - try: - await self._cleanup_task - except asyncio.CancelledError: - pass - self._cleanup_task = None - - async def _cleanup_expired_chunks(self): - """清理过期的切片缓冲区""" - while True: - try: - await asyncio.sleep(10) # 每10秒检查一次 - current_time = time.time() - - expired_chunks = [] - for chunk_id, buffer_info in self.chunk_buffers.items(): - if current_time - buffer_info["timestamp"] > self.timeout: - expired_chunks.append(chunk_id) - - for chunk_id in expired_chunks: - logger.warning(f"清理过期的切片缓冲区: {chunk_id}") - del self.chunk_buffers[chunk_id] - - except asyncio.CancelledError: - break - except Exception as e: - logger.error(f"清理过期切片时出错: {e}") - - async def add_chunk(self, message: Union[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """ - 添加切片,如果切片完整则返回重组后的消息 - - Args: - message: 切片消息(字符串或字典) - - Returns: - 如果切片完整则返回重组后的原始消息字典,否则返回None - """ - try: - # 统一转换为字典 - if isinstance(message, str): - chunk_data = orjson.loads(message) - else: - chunk_data = message - - # 检查是否是切片消息 - if not chunker.is_chunk_message(chunk_data): - # 不是切片消息,直接返回 - if "_original_message" in chunk_data: - # 这是一个被包装的非切片消息,解包返回 - try: - return orjson.loads(chunk_data["_original_message"]) - except orjson.JSONDecodeError: - return {"text_message": chunk_data["_original_message"]} - else: - return chunk_data - - chunk_info = chunk_data["__mmc_chunk_info__"] - chunk_content = chunk_data["__mmc_chunk_data__"] - - chunk_id = chunk_info["chunk_id"] - chunk_index = chunk_info["chunk_index"] - total_chunks = chunk_info["total_chunks"] - chunk_timestamp = chunk_info.get("timestamp", time.time()) - - # 初始化缓冲区 - if chunk_id not in self.chunk_buffers: - self.chunk_buffers[chunk_id] = { - "chunks": {}, - "total_chunks": total_chunks, - "received_chunks": 0, - "timestamp": chunk_timestamp, - } - - buffer = self.chunk_buffers[chunk_id] - - # 检查切片是否已经接收过 - if chunk_index in buffer["chunks"]: - logger.warning(f"重复接收切片: {chunk_id}#{chunk_index}") - return None - - # 添加切片 - buffer["chunks"][chunk_index] = chunk_content - buffer["received_chunks"] += 1 - buffer["timestamp"] = time.time() # 更新时间戳 - - logger.debug(f"接收切片: {chunk_id}#{chunk_index} ({buffer['received_chunks']}/{total_chunks})") - - # 检查是否接收完整 - if buffer["received_chunks"] == total_chunks: - # 重组消息 - reassembled_message = "" - for i in range(total_chunks): - if i not in buffer["chunks"]: - logger.error(f"切片 {chunk_id}#{i} 缺失,无法重组") - return None - reassembled_message += buffer["chunks"][i] - - # 清理缓冲区 - del self.chunk_buffers[chunk_id] - - logger.debug(f"消息重组完成: {chunk_id} ({len(reassembled_message)} chars)") - - # 尝试反序列化重组后的消息 - try: - return orjson.loads(reassembled_message) - except orjson.JSONDecodeError: - # 如果不能反序列化为JSON,则作为文本消息返回 - return {"text_message": reassembled_message} - - return None - - except (orjson.JSONDecodeError, KeyError, TypeError) as e: - logger.error(f"处理切片消息时出错: {e}") - return None - - def get_pending_chunks_info(self) -> Dict[str, Any]: - """获取待处理切片信息""" - info = {} - for chunk_id, buffer in self.chunk_buffers.items(): - info[chunk_id] = { - "received": buffer["received_chunks"], - "total": buffer["total_chunks"], - "progress": f"{buffer['received_chunks']}/{buffer['total_chunks']}", - "age_seconds": time.time() - buffer["timestamp"], - } - return info - - -# 全局实例 -chunker = MessageChunker() -reassembler = MessageReassembler() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py b/src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py deleted file mode 100644 index 77d9a3939..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py +++ /dev/null @@ -1,61 +0,0 @@ -from mofox_wire import RouteConfig, Router, TargetConfig - -from src.common.logger import get_logger -from src.common.server import get_global_server -from src.plugin_system.apis import config_api - -from .send_handler import send_handler - -logger = get_logger("napcat_adapter") - -router = None - - -def create_router(plugin_config: dict): - """创建路由器实例""" - global router - platform_name = config_api.get_plugin_config(plugin_config, "maibot_server.platform_name", "qq") - - # 优先从插件配置读取 host 和 port,如果不存在则回退到全局配置 - config_host = config_api.get_plugin_config(plugin_config, "maibot_server.host", "") - config_port = config_api.get_plugin_config(plugin_config, "maibot_server.port", 0) - - if config_host and config_port > 0: - # 使用插件配置 - host = config_host - port = config_port - logger.debug(f"初始化MoFox-Bot连接,使用插件配置地址:{host}:{port}") - else: - # 回退到全局配置 - server = get_global_server() - host = server.host - port = server.port - logger.debug(f"初始化MoFox-Bot连接,使用全局配置地址:{host}:{port}") - - route_config = RouteConfig( - route_config={ - platform_name: TargetConfig( - url=f"ws://{host}:{port}/ws", - token=None, - ) - } - ) - router = Router(route_config) - return router - - -async def mmc_start_com(plugin_config: dict | None = None): - """启动MoFox-Bot连接""" - logger.debug("正在连接MoFox-Bot") - if plugin_config: - create_router(plugin_config) - - if router: - router.register_class_handler(send_handler.handle_message) - await router.run() - - -async def mmc_stop_com(): - """停止MoFox-Bot连接""" - if router: - await router.stop() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py deleted file mode 100644 index d016a5be4..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py +++ /dev/null @@ -1,92 +0,0 @@ -from enum import Enum - - -class MetaEventType: - lifecycle = "lifecycle" # 生命周期 - - class Lifecycle: - connect = "connect" # 生命周期 - WebSocket 连接成功 - - heartbeat = "heartbeat" # 心跳 - - -class MessageType: # 接受消息大类 - private = "private" # 私聊消息 - - class Private: - friend = "friend" # 私聊消息 - 好友 - group = "group" # 私聊消息 - 群临时 - group_self = "group_self" # 私聊消息 - 群中自身发送 - other = "other" # 私聊消息 - 其他 - - group = "group" # 群聊消息 - - class Group: - normal = "normal" # 群聊消息 - 普通 - anonymous = "anonymous" # 群聊消息 - 匿名消息 - notice = "notice" # 群聊消息 - 系统提示 - - -class NoticeType: # 通知事件 - friend_recall = "friend_recall" # 私聊消息撤回 - group_recall = "group_recall" # 群聊消息撤回 - notify = "notify" - group_ban = "group_ban" # 群禁言 - group_msg_emoji_like = "group_msg_emoji_like" # 群聊表情回复 - group_upload = "group_upload" # 群文件上传 - - class Notify: - poke = "poke" # 戳一戳 - input_status = "input_status" # 正在输入 - - class GroupBan: - ban = "ban" # 禁言 - lift_ban = "lift_ban" # 解除禁言 - - -class RealMessageType: # 实际消息分类 - text = "text" # 纯文本 - face = "face" # qq表情 - image = "image" # 图片 - record = "record" # 语音 - video = "video" # 视频 - at = "at" # @某人 - rps = "rps" # 猜拳魔法表情 - dice = "dice" # 骰子 - shake = "shake" # 私聊窗口抖动(只收) - poke = "poke" # 群聊戳一戳 - share = "share" # 链接分享(json形式) - reply = "reply" # 回复消息 - forward = "forward" # 转发消息 - node = "node" # 转发消息节点 - json = "json" # json消息 - file = "file" # 文件 - - -class MessageSentType: - private = "private" - - class Private: - friend = "friend" - group = "group" - - group = "group" - - class Group: - normal = "normal" - - -class CommandType(Enum): - """命令类型""" - - GROUP_BAN = "set_group_ban" # 禁言用户 - GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言 - GROUP_KICK = "set_group_kick" # 踢出群聊 - SEND_POKE = "send_poke" # 戳一戳 - DELETE_MSG = "delete_msg" # 撤回消息 - - def __str__(self) -> str: - return self.value - - -ACCEPT_FORMAT = ["text", "image", "emoji", "reply", "voice", "command", "voiceurl", "music", "videourl", "file"] diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py deleted file mode 100644 index b7e7b2c25..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py +++ /dev/null @@ -1,1183 +0,0 @@ -import base64 -import orjson -import time -import uuid -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple - -import websockets as Server -from maim_message import ( - BaseMessageInfo, - FormatInfo, - GroupInfo, - MessageBase, - Seg, - TemplateInfo, - UserInfo, -) - -from src.common.logger import get_logger -from src.plugin_system.apis import config_api -from src.plugin_system.core.event_manager import event_manager - -from ...CONSTS import PLUGIN_NAME -from ...event_types import NapcatEvent -from ..response_pool import get_response -from ..utils import ( - get_group_info, - get_image_base64, - get_member_info, - get_message_detail, - get_record_detail, - get_self_info, -) -from ..video_handler import get_video_downloader -from ..websocket_manager import websocket_manager -from . import ACCEPT_FORMAT, MessageType, RealMessageType -from .message_sending import message_send_instance -from .qq_emoji_list import qq_face - -logger = get_logger("napcat_adapter") - - -class MessageHandler: - def __init__(self): - self.server_connection: Server.ServerConnection = None - self.bot_id_list: Dict[int, bool] = {} - self.plugin_config = None - # 消息缓冲功能已移除 - - def set_plugin_config(self, plugin_config: dict): - """设置插件配置""" - self.plugin_config = plugin_config - # 消息缓冲功能已移除 - - async def shutdown(self): - """关闭消息处理器,清理资源""" - # 消息缓冲功能已移除 - - # 消息缓冲功能已移除 - - async def set_server_connection(self, server_connection: Server.ServerConnection) -> None: - """设置Napcat连接""" - self.server_connection = server_connection - - def get_server_connection(self) -> Server.ServerConnection: - """获取当前的服务器连接""" - # 优先使用直接设置的连接,否则从 websocket_manager 获取 - if self.server_connection: - return self.server_connection - return websocket_manager.get_connection() - - async def check_allow_to_chat( - self, - user_id: int, - group_id: Optional[int] = None, - ignore_bot: Optional[bool] = False, - ignore_global_list: Optional[bool] = False, - ) -> bool: - # sourcery skip: hoist-statement-from-if, merge-else-if-into-elif - """ - 检查是否允许聊天 - Parameters: - user_id: int: 用户ID - group_id: int: 群ID - ignore_bot: bool: 是否忽略机器人检查 - ignore_global_list: bool: 是否忽略全局黑名单检查 - Returns: - bool: 是否允许聊天 - """ - logger.debug(f"群聊id: {group_id}, 用户id: {user_id}") - logger.debug("开始检查聊天白名单/黑名单") - - # 使用新的权限管理器检查权限 - if group_id: - # 检查群聊黑白名单 - group_list_type = config_api.get_plugin_config(self.plugin_config, "features.group_list_type", "blacklist") - group_list = config_api.get_plugin_config(self.plugin_config, "features.group_list", []) - - if group_list_type == "whitelist": - if group_id not in group_list: - logger.warning("群聊不在白名单中,消息被丢弃") - return False - else: # blacklist - if group_id in group_list: - logger.warning("群聊在黑名单中,消息被丢弃") - return False - else: - # 检查私聊黑白名单 - private_list_type = config_api.get_plugin_config( - self.plugin_config, "features.private_list_type", "blacklist" - ) - private_list = config_api.get_plugin_config(self.plugin_config, "features.private_list", []) - - if private_list_type == "whitelist": - if user_id not in private_list: - logger.warning("私聊不在白名单中,消息被丢弃") - return False - else: # blacklist - if user_id in private_list: - logger.warning("私聊在黑名单中,消息被丢弃") - return False - - # 检查全局禁止名单 - ban_user_id = config_api.get_plugin_config(self.plugin_config, "features.ban_user_id", []) - if not ignore_global_list and user_id in ban_user_id: - logger.warning("用户在全局黑名单中,消息被丢弃") - return False - - # 检查QQ官方机器人 - ban_qq_bot = config_api.get_plugin_config(self.plugin_config, "features.ban_qq_bot", False) - if ban_qq_bot and group_id and not ignore_bot: - logger.debug("开始判断是否为机器人") - member_info = await get_member_info(self.get_server_connection(), group_id, user_id) - if member_info: - is_bot = member_info.get("is_robot") - if is_bot is None: - logger.warning("无法获取用户是否为机器人,默认为不是但是不进行更新") - else: - if is_bot: - logger.warning("QQ官方机器人消息拦截已启用,消息被丢弃,新机器人加入拦截名单") - self.bot_id_list[user_id] = True - return False - else: - self.bot_id_list[user_id] = False - - return True - - async def handle_raw_message(self, raw_message: dict) -> None: - # sourcery skip: low-code-quality, remove-unreachable-code - """ - 从Napcat接受的原始消息处理 - - Parameters: - raw_message: dict: 原始消息 - """ - - # 添加原始消息调试日志,特别关注message字段 - logger.debug( - f"收到原始消息: message_type={raw_message.get('message_type')}, message_id={raw_message.get('message_id')}" - ) - logger.debug(f"原始消息内容: {raw_message.get('message', [])}") - - # 检查是否包含@或video消息段 - message_segments = raw_message.get("message", []) - if message_segments: - for i, seg in enumerate(message_segments): - seg_type = seg.get("type") - if seg_type in ["at", "video"]: - logger.info(f"检测到 {seg_type.upper()} 消息段 [{i}]: {seg}") - elif seg_type not in ["text", "face", "image"]: - logger.warning(f"检测到特殊消息段 [{i}]: type={seg_type}, data={seg.get('data', {})}") - - message_type: str = raw_message.get("message_type") - message_id: int = raw_message.get("message_id") - # message_time: int = raw_message.get("time") - message_time: float = time.time() # 应可乐要求,现在是float了 - - template_info: TemplateInfo = None # 模板信息,暂时为空,等待启用 - if message_type == MessageType.private: - sub_type = raw_message.get("sub_type") - if sub_type == MessageType.Private.friend: - sender_info: dict = raw_message.get("sender") - - if not await self.check_allow_to_chat(sender_info.get("user_id"), None): - return None - - # 发送者用户信息 - user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), - user_id=sender_info.get("user_id"), - user_nickname=sender_info.get("nickname"), - user_cardname=sender_info.get("card"), - ) - - # 不存在群信息 - group_info: GroupInfo = None - elif sub_type == MessageType.Private.group: - """ - 本部分暂时不做支持,先放着 - """ - logger.warning("群临时消息类型不支持") - return None - - sender_info: dict = raw_message.get("sender") - - # 由于临时会话中,Napcat默认不发送成员昵称,所以需要单独获取 - fetched_member_info: dict = await get_member_info( - self.get_server_connection(), - raw_message.get("group_id"), - sender_info.get("user_id"), - ) - nickname = fetched_member_info.get("nickname") if fetched_member_info else None - # 发送者用户信息 - user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), - user_id=sender_info.get("user_id"), - user_nickname=nickname, - user_cardname=None, - ) - - # -------------------这里需要群信息吗?------------------- - - # 获取群聊相关信息,在此单独处理group_name,因为默认发送的消息中没有 - fetched_group_info: dict = await get_group_info( - self.get_server_connection(), raw_message.get("group_id") - ) - group_name = "" - if fetched_group_info.get("group_name"): - group_name = fetched_group_info.get("group_name") - - group_info: GroupInfo = GroupInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), - group_id=raw_message.get("group_id"), - group_name=group_name, - ) - - else: - logger.warning(f"私聊消息类型 {sub_type} 不支持") - return None - elif message_type == MessageType.group: - sub_type = raw_message.get("sub_type") - if sub_type == MessageType.Group.normal: - sender_info: dict = raw_message.get("sender") - - if not await self.check_allow_to_chat(sender_info.get("user_id"), raw_message.get("group_id")): - return None - - # 发送者用户信息 - user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), - user_id=sender_info.get("user_id"), - user_nickname=sender_info.get("nickname"), - user_cardname=sender_info.get("card"), - ) - - # 获取群聊相关信息,在此单独处理group_name,因为默认发送的消息中没有 - fetched_group_info = await get_group_info(self.get_server_connection(), raw_message.get("group_id")) - group_name: str = None - if fetched_group_info: - group_name = fetched_group_info.get("group_name") - - group_info: GroupInfo = GroupInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), - group_id=raw_message.get("group_id"), - group_name=group_name, - ) - - else: - logger.warning(f"群聊消息类型 {sub_type} 不支持") - return None - - # 处理实际信息 - if not raw_message.get("message"): - logger.warning("原始消息内容为空") - return None - - # 获取Seg列表 - seg_message: List[Seg] = await self.handle_real_message(raw_message) - if not seg_message: - logger.warning("处理后消息内容为空") - return None - - # 动态生成 content_format - content_formats = sorted(list(set(seg.type for seg in seg_message))) - logger.debug(f"动态生成 content_format: {content_formats}") - format_info: FormatInfo = FormatInfo( - content_format=content_formats, - accept_format=ACCEPT_FORMAT, - ) - - additional_config: dict = {} - if config_api.get_plugin_config(self.plugin_config, "voice.use_tts"): - additional_config["allow_tts"] = True - - # 消息信息 - message_info: BaseMessageInfo = BaseMessageInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), - message_id=message_id, - time=message_time, - user_info=user_info, - group_info=group_info, - template_info=template_info, - format_info=format_info, - additional_config=additional_config, - ) - - # 消息缓冲功能已移除,直接处理消息 - - logger.debug(f"准备发送消息到MoFox-Bot,消息段数量: {len(seg_message)}") - for i, seg in enumerate(seg_message): - logger.debug(f"消息段 {i}: type={seg.type}, data={str(seg.data)[:100]}...") - - submit_seg: Seg = Seg( - type="seglist", - data=seg_message, - ) - # MessageBase创建 - message_base: MessageBase = MessageBase( - message_info=message_info, - message_segment=submit_seg, - raw_message=raw_message.get("raw_message"), - ) - - logger.debug("发送到Maibot处理信息") - await message_send_instance.message_send(message_base) - - async def handle_real_message(self, raw_message: dict, in_reply: bool = False) -> List[Seg] | None: - # sourcery skip: low-code-quality - """ - 处理实际消息 - Parameters: - real_message: dict: 实际消息 - Returns: - seg_message: list[Seg]: 处理后的消息段列表 - """ - real_message: list = raw_message.get("message") - if not real_message: - return None - seg_message: List[Seg] = [] - for sub_message in real_message: - sub_message: dict - sub_message_type = sub_message.get("type") - - # 添加详细的消息类型调试信息 - logger.debug(f"处理消息段: type={sub_message_type}, data={sub_message.get('data', {})}") - - # 特别关注 at 和 video 消息的识别 - if sub_message_type == "at": - logger.debug(f"检测到@消息: {sub_message}") - elif sub_message_type == "video": - logger.debug(f"检测到VIDEO消息: {sub_message}") - elif sub_message_type not in ["text", "face", "image", "record"]: - logger.warning(f"检测到特殊消息类型: {sub_message_type}, 完整消息: {sub_message}") - - match sub_message_type: - case RealMessageType.text: - ret_seg = await self.handle_text_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.TEXT, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - else: - logger.warning("text处理失败") - case RealMessageType.face: - ret_seg = await self.handle_face_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.FACE, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - else: - logger.warning("face处理失败或不支持") - case RealMessageType.reply: - if not in_reply: - ret_seg = await self.handle_reply_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.REPLY, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message += ret_seg - else: - logger.warning("reply处理失败") - case RealMessageType.image: - logger.debug("开始处理图片消息段") - ret_seg = await self.handle_image_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.IMAGE, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - logger.debug("图片处理成功,添加到消息段") - else: - logger.warning("image处理失败") - logger.debug("图片消息段处理完成") - case RealMessageType.record: - ret_seg = await self.handle_record_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.RECORD, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.clear() - seg_message.append(ret_seg) - break # 使得消息只有record消息 - else: - logger.warning("record处理失败或不支持") - case RealMessageType.video: - logger.debug(f"开始处理VIDEO消息段: {sub_message}") - ret_seg = await self.handle_video_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.VIDEO, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - else: - logger.warning(f"video处理失败,原始消息: {sub_message}") - case RealMessageType.at: - logger.debug(f"开始处理AT消息段: {sub_message}") - ret_seg = await self.handle_at_message( - sub_message, - raw_message.get("self_id"), - raw_message.get("group_id"), - ) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.AT, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - else: - logger.warning(f"at处理失败,原始消息: {sub_message}") - case RealMessageType.rps: - ret_seg = await self.handle_rps_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.RPS, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - else: - logger.warning("rps处理失败") - case RealMessageType.dice: - ret_seg = await self.handle_dice_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.DICE, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - else: - logger.warning("dice处理失败") - case RealMessageType.shake: - ret_seg = await self.handle_shake_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.SHAKE, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - else: - logger.warning("shake处理失败") - case RealMessageType.share: - print( - "\n\n哦哦哦噢噢噢哦哦你收到了一个超级无敌SHARE消息,快速速把你刚刚收到的消息截图发到MoFox-Bot群里!!!!\n\n" - ) - logger.warning("暂时不支持链接解析") - case RealMessageType.forward: - messages = await self._get_forward_message(sub_message) - if not messages: - logger.warning("转发消息内容为空或获取失败") - return None - ret_seg = await self.handle_forward_message(messages) - if ret_seg: - seg_message.append(ret_seg) - else: - logger.warning("转发消息处理失败") - case RealMessageType.node: - print( - "\n\n哦哦哦噢噢噢哦哦你收到了一个超级无敌NODE消息,快速速把你刚刚收到的消息截图发到MoFox-Bot群里!!!!\n\n" - ) - logger.warning("不支持转发消息节点解析") - case RealMessageType.json: - ret_seg = await self.handle_json_message(sub_message) - if ret_seg: - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.JSON, permission_group=PLUGIN_NAME, message_seg=ret_seg - ) - seg_message.append(ret_seg) - else: - logger.warning("json处理失败") - case RealMessageType.file: - ret_seg = await self.handle_file_message(sub_message) - if ret_seg: - # NapcatEvent doesn't have a FILE event yet, so we won't trigger one for now. - seg_message.append(ret_seg) - else: - logger.warning("file处理失败") - case _: - logger.warning(f"未知消息类型: {sub_message_type}") - - logger.debug(f"handle_real_message完成,处理了{len(real_message)}个消息段,生成了{len(seg_message)}个seg") - return seg_message - - async def handle_text_message(self, raw_message: dict) -> Seg: - """ - 处理纯文本信息 - Parameters: - raw_message: dict: 原始消息 - Returns: - seg_data: Seg: 处理后的消息段 - """ - message_data: dict = raw_message.get("data") - plain_text: str = message_data.get("text") - return Seg(type="text", data=plain_text) - - async def handle_face_message(self, raw_message: dict) -> Seg | None: - """ - 处理表情消息 - Parameters: - raw_message: dict: 原始消息 - Returns: - seg_data: Seg: 处理后的消息段 - """ - message_data: dict = raw_message.get("data") - face_raw_id: str = str(message_data.get("id")) - if face_raw_id in qq_face: - face_content: str = qq_face.get(face_raw_id) - return Seg(type="text", data=face_content) - else: - logger.warning(f"不支持的表情:{face_raw_id}") - return None - - async def handle_image_message(self, raw_message: dict) -> Seg | None: - """ - 处理图片消息与表情包消息 - Parameters: - raw_message: dict: 原始消息 - Returns: - seg_data: Seg: 处理后的消息段 - """ - message_data: dict = raw_message.get("data") - image_sub_type = message_data.get("sub_type") - try: - image_base64 = await get_image_base64(message_data.get("url")) - except Exception as e: - logger.error(f"图片消息处理失败: {str(e)}") - return None - if image_sub_type == 0: - """这部分认为是图片""" - return Seg(type="image", data=image_base64) - elif image_sub_type not in [4, 9]: - """这部分认为是表情包""" - return Seg(type="emoji", data=image_base64) - else: - logger.warning(f"不支持的图片子类型:{image_sub_type}") - return None - - async def handle_at_message(self, raw_message: dict, self_id: int, group_id: int) -> Seg | None: - # sourcery skip: use-named-expression - """ - 处理at消息 - Parameters: - raw_message: dict: 原始消息 - self_id: int: 机器人QQ号 - group_id: int: 群号 - Returns: - seg_data: Seg: 处理后的消息段 - """ - message_data: dict = raw_message.get("data") - if message_data: - qq_id = message_data.get("qq") - if str(self_id) == str(qq_id): - logger.debug("机器人被at") - self_info: dict = await get_self_info(self.get_server_connection()) - if self_info: - # 返回包含昵称和用户ID的at格式,便于后续处理 - return Seg(type="at", data=f"{self_info.get('nickname')}:{self_info.get('user_id')}") - else: - return None - else: - member_info: dict = await get_member_info( - self.get_server_connection(), group_id=group_id, user_id=qq_id - ) - if member_info: - # 返回包含昵称和用户ID的at格式,便于后续处理 - return Seg(type="at", data=f"{member_info.get('nickname')}:{member_info.get('user_id')}") - else: - return None - - async def handle_record_message(self, raw_message: dict) -> Seg | None: - """ - 处理语音消息 - Parameters: - raw_message: dict: 原始消息 - Returns: - seg_data: Seg: 处理后的消息段 - """ - message_data: dict = raw_message.get("data") - file: str = message_data.get("file") - if not file: - logger.warning("语音消息缺少文件信息") - return None - try: - record_detail = await get_record_detail(self.get_server_connection(), file) - if not record_detail: - logger.warning("获取语音消息详情失败") - return None - audio_base64: str = record_detail.get("base64") - except Exception as e: - logger.error(f"语音消息处理失败: {str(e)}") - return None - if not audio_base64: - logger.error("语音消息处理失败,未获取到音频数据") - return None - return Seg(type="voice", data=audio_base64) - - async def handle_video_message(self, raw_message: dict) -> Seg | None: - """ - 处理视频消息 - Parameters: - raw_message: dict: 原始消息 - Returns: - seg_data: Seg: 处理后的消息段 - """ - message_data: dict = raw_message.get("data") - - # 添加详细的调试信息 - logger.debug(f"视频消息原始数据: {raw_message}") - logger.debug(f"视频消息数据: {message_data}") - - # QQ视频消息可能包含url或filePath字段 - video_url = message_data.get("url") - file_path = message_data.get("filePath") or message_data.get("file_path") - - logger.debug(f"视频URL: {video_url}") - logger.debug(f"视频文件路径: {file_path}") - - # 优先使用本地文件路径,其次使用URL - video_source = file_path if file_path else video_url - - if not video_source: - logger.warning("视频消息缺少URL或文件路径信息") - logger.warning(f"完整消息数据: {message_data}") - return None - - try: - # 检查是否为本地文件路径 - if file_path and Path(file_path).exists(): - logger.debug(f"使用本地视频文件: {file_path}") - # 直接读取本地文件 - with open(file_path, "rb") as f: - video_data = f.read() - - # 将视频数据编码为base64用于传输 - video_base64 = base64.b64encode(video_data).decode("utf-8") - logger.debug(f"视频文件大小: {len(video_data) / (1024 * 1024):.2f} MB") - - # 返回包含详细信息的字典格式 - return Seg( - type="video", - data={ - "base64": video_base64, - "filename": Path(file_path).name, - "size_mb": len(video_data) / (1024 * 1024), - }, - ) - - elif video_url: - logger.debug(f"使用视频URL下载: {video_url}") - # 使用video_handler下载视频 - video_downloader = get_video_downloader() - download_result = await video_downloader.download_video(video_url) - - if not download_result["success"]: - logger.warning(f"视频下载失败: {download_result.get('error', '未知错误')}") - logger.warning(f"失败的URL: {video_url}") - return None - - # 将视频数据编码为base64用于传输 - video_base64 = base64.b64encode(download_result["data"]).decode("utf-8") - logger.debug(f"视频下载成功,大小: {len(download_result['data']) / (1024 * 1024):.2f} MB") - - # 返回包含详细信息的字典格式 - return Seg( - type="video", - data={ - "base64": video_base64, - "filename": download_result.get("filename", "video.mp4"), - "size_mb": len(download_result["data"]) / (1024 * 1024), - "url": video_url, - }, - ) - - else: - logger.warning("既没有有效的本地文件路径,也没有有效的视频URL") - return None - - except Exception as e: - logger.error(f"视频消息处理失败: {str(e)}") - logger.error(f"视频源: {video_source}") - return None - - async def handle_reply_message(self, raw_message: dict) -> List[Seg] | None: - # sourcery skip: move-assign-in-block, use-named-expression - """ - 处理回复消息 - - """ - raw_message_data: dict = raw_message.get("data") - message_id: int = None - if raw_message_data: - message_id = raw_message_data.get("id") - else: - return None - message_detail: dict = await get_message_detail(self.get_server_connection(), message_id) - if not message_detail: - logger.warning("获取被引用的消息详情失败") - return None - reply_message = await self.handle_real_message(message_detail, in_reply=True) - if reply_message is None: - reply_message = [Seg(type="text", data="(获取发言内容失败)")] - sender_info: dict = message_detail.get("sender") - sender_nickname: str = sender_info.get("nickname") - sender_id = sender_info.get("user_id") - seg_message: List[Seg] = [] - if not sender_nickname: - logger.warning("无法获取被引用的人的昵称,返回默认值") - seg_message.append(Seg(type="text", data="[回复 未知用户:")) - else: - if sender_id: - seg_message.append(Seg(type="text", data=f"[回复<{sender_nickname}({sender_id})>:")) - else: - seg_message.append(Seg(type="text", data=f"[回复<{sender_nickname}>:")) - seg_message += reply_message - seg_message.append(Seg(type="text", data="],说:")) - return seg_message - - async def handle_forward_message(self, message_list: list) -> Seg | None: - """ - 递归处理转发消息,并按照动态方式确定图片处理方式 - Parameters: - message_list: list: 转发消息列表 - """ - handled_message, image_count = await self._handle_forward_message(message_list, 0) - handled_message: Seg - image_count: int - if not handled_message: - return None - - processed_message: Seg - if image_count < 5 and image_count > 0: - # 处理图片数量小于5的情况,此时解析图片为base64 - logger.debug("图片数量小于5,开始解析图片为base64") - processed_message = await self._recursive_parse_image_seg(handled_message, True) - elif image_count > 0: - logger.debug("图片数量大于等于5,开始解析图片为占位符") - # 处理图片数量大于等于5的情况,此时解析图片为占位符 - processed_message = await self._recursive_parse_image_seg(handled_message, False) - else: - # 处理没有图片的情况,此时直接返回 - logger.debug("没有图片,直接返回") - processed_message = handled_message - - # 添加转发消息提示 - forward_hint = Seg(type="text", data="这是一条转发消息:\n") - return Seg(type="seglist", data=[forward_hint, processed_message]) - - async def handle_dice_message(self, raw_message: dict) -> Seg: - message_data: dict = raw_message.get("data", {}) - res = message_data.get("result", "") - return Seg(type="text", data=f"[扔了一个骰子,点数是{res}]") - - async def handle_shake_message(self, raw_message: dict) -> Seg: - return Seg(type="text", data="[向你发送了窗口抖动,现在你的屏幕猛烈地震了一下!]") - - async def handle_json_message(self, raw_message: dict) -> Seg: - """ - 处理JSON消息 - Parameters: - raw_message: dict: 原始消息 - Returns: - seg_data: Seg: 处理后的消息段 - """ - message_data: dict = raw_message.get("data", {}) - json_data = message_data.get("data", "") - - # 检查JSON消息格式 - if not message_data or "data" not in message_data: - logger.warning("JSON消息格式不正确") - return Seg(type="json", data=orjson.dumps(message_data).decode('utf-8')) - - try: - # 尝试将json_data解析为Python对象 - nested_data = orjson.loads(json_data) - - # 检查是否是机器人自己上传文件的回声 - if self._is_file_upload_echo(nested_data): - logger.info("检测到机器人发送文件的回声消息,将作为文件消息处理") - # 从回声消息中提取文件信息 - file_info = self._extract_file_info_from_echo(nested_data) - if file_info: - # 构建一个与普通文件消息格式相同的字典 - file_message_dict = {"type": "file", "data": file_info} - return await self.handle_file_message(file_message_dict) - - # 检查是否是QQ小程序分享消息 - if "app" in nested_data and "com.tencent.miniapp" in str(nested_data.get("app", "")): - logger.debug("检测到QQ小程序分享消息,开始提取信息") - - # 提取目标字段 - extracted_info = {} - - # 提取 meta.detail_1 中的信息 - meta = nested_data.get("meta", {}) - detail_1 = meta.get("detail_1", {}) - - if detail_1: - extracted_info["title"] = detail_1.get("title", "") - extracted_info["desc"] = detail_1.get("desc", "") - qqdocurl = detail_1.get("qqdocurl", "") - - # 从qqdocurl中提取b23.tv短链接 - if qqdocurl and "b23.tv" in qqdocurl: - # 查找b23.tv链接的起始位置 - start_pos = qqdocurl.find("https://b23.tv/") - if start_pos != -1: - # 提取从https://b23.tv/开始的部分 - b23_part = qqdocurl[start_pos:] - # 查找第一个?的位置,截取到?之前 - question_pos = b23_part.find("?") - if question_pos != -1: - extracted_info["short_url"] = b23_part[:question_pos] - else: - extracted_info["short_url"] = b23_part - else: - extracted_info["short_url"] = qqdocurl - else: - extracted_info["short_url"] = qqdocurl - - # 如果成功提取到关键信息,返回格式化的文本 - if extracted_info.get("title") or extracted_info.get("desc") or extracted_info.get("short_url"): - content_parts = [] - - if extracted_info.get("title"): - content_parts.append(f"来源: {extracted_info['title']}") - - if extracted_info.get("desc"): - content_parts.append(f"标题: {extracted_info['desc']}") - - if extracted_info.get("short_url"): - content_parts.append(f"链接: {extracted_info['short_url']}") - - formatted_content = "\n".join(content_parts) - return Seg( - type="text", - data=f"这是一条小程序分享消息,可以根据来源,考虑使用对应解析工具\n{formatted_content}", - ) - - # 检查是否是音乐分享 (QQ音乐类型) - if nested_data.get("view") == "music" and "com.tencent.music" in str(nested_data.get("app", "")): - meta = nested_data.get("meta", {}) - music = meta.get("music", {}) - if music: - tag = music.get("tag", "未知来源") - logger.debug(f"检测到【{tag}】音乐分享消息 (music view),开始提取信息") - - title = music.get("title", "未知歌曲") - desc = music.get("desc", "未知艺术家") - jump_url = music.get("jumpUrl", "") - preview_url = music.get("preview", "") - - artist = "未知艺术家" - song_title = title - - if "网易云音乐" in tag: - artist = desc - elif "QQ音乐" in tag: - if " - " in title: - parts = title.split(" - ", 1) - song_title = parts[0] - artist = parts[1] - else: - artist = desc - - formatted_content = ( - f"这是一张来自【{tag}】的音乐分享卡片:\n" - f"歌曲: {song_title}\n" - f"艺术家: {artist}\n" - f"跳转链接: {jump_url}\n" - f"封面图: {preview_url}" - ) - return Seg(type="text", data=formatted_content) - - # 检查是否是新闻/图文分享 (网易云音乐可能伪装成这种) - elif nested_data.get("view") == "news" and "com.tencent.tuwen" in str(nested_data.get("app", "")): - meta = nested_data.get("meta", {}) - news = meta.get("news", {}) - if news and "网易云音乐" in news.get("tag", ""): - tag = news.get("tag") - logger.debug(f"检测到【{tag}】音乐分享消息 (news view),开始提取信息") - - title = news.get("title", "未知歌曲") - desc = news.get("desc", "未知艺术家") - jump_url = news.get("jumpUrl", "") - preview_url = news.get("preview", "") - - formatted_content = ( - f"这是一张来自【{tag}】的音乐分享卡片:\n" - f"标题: {title}\n" - f"描述: {desc}\n" - f"跳转链接: {jump_url}\n" - f"封面图: {preview_url}" - ) - return Seg(type="text", data=formatted_content) - - # 如果没有提取到关键信息,返回None - return None - - except orjson.JSONDecodeError: - # 如果解析失败,我们假设它不是我们关心的任何一种结构化JSON, - # 而是普通的文本或者无法解析的格式。 - logger.debug(f"无法将data字段解析为JSON: {json_data}") - return None - except Exception as e: - logger.error(f"处理JSON消息时发生未知错误: {e}") - return None - - async def handle_file_message(self, raw_message: dict) -> Seg | None: - """ - 处理文件消息 - Parameters: - raw_message: dict: 原始消息 - Returns: - seg_data: Seg: 处理后的消息段 - """ - message_data: dict = raw_message.get("data") - if not message_data: - logger.warning("文件消息缺少 data 字段") - return None - - # 提取文件信息 - file_name = message_data.get("file") - file_size = message_data.get("file_size") - file_id = message_data.get("file_id") - - logger.info(f"收到文件消息: name={file_name}, size={file_size}, id={file_id}") - - # 将文件信息打包成字典 - file_data = { - "name": file_name, - "size": file_size, - "id": file_id, - } - - return Seg(type="file", data=file_data) - - def _is_file_upload_echo(self, nested_data: Any) -> bool: - """检查一个JSON对象是否是机器人自己上传文件的回声消息""" - if not isinstance(nested_data, dict): - return False - - # 检查 'app' 和 'meta' 字段是否存在 - if "app" not in nested_data or "meta" not in nested_data: - return False - - # 检查 'app' 字段是否包含 'com.tencent.miniapp' - if "com.tencent.miniapp" not in str(nested_data.get("app", "")): - return False - - # 检查 'meta' 内部的 'detail_1' 的 'busi_id' 是否为 '1014' - meta = nested_data.get("meta", {}) - detail_1 = meta.get("detail_1", {}) - if detail_1.get("busi_id") == "1014": - return True - - return False - - def _extract_file_info_from_echo(self, nested_data: dict) -> Optional[dict]: - """从文件上传的回声消息中提取文件信息""" - try: - meta = nested_data.get("meta", {}) - detail_1 = meta.get("detail_1", {}) - - # 文件名在 'desc' 字段 - file_name = detail_1.get("desc") - - # 文件大小在 'summary' 字段,格式为 "大小:1.7MB" - summary = detail_1.get("summary", "") - file_size_str = summary.replace("大小:", "").strip() # 移除前缀和空格 - - # QQ API有时返回的大小不标准,这里我们只提取它给的字符串 - # 实际大小已经由Napcat在发送时记录,这里主要是为了保持格式一致 - - if file_name and file_size_str: - return {"file": file_name, "file_size": file_size_str, "file_id": None} # file_id在回声中不可用 - except Exception as e: - logger.error(f"从文件回声中提取信息失败: {e}") - - return None - - async def handle_rps_message(self, raw_message: dict) -> Seg: - message_data: dict = raw_message.get("data", {}) - res = message_data.get("result", "") - if res == "1": - shape = "布" - elif res == "2": - shape = "剪刀" - else: - shape = "石头" - return Seg(type="text", data=f"[发送了一个魔法猜拳表情,结果是:{shape}]") - - async def _recursive_parse_image_seg(self, seg_data: Seg, to_image: bool) -> Seg: - # sourcery skip: merge-else-if-into-elif - if to_image: - if seg_data.type == "seglist": - new_seg_list = [] - for i_seg in seg_data.data: - parsed_seg = await self._recursive_parse_image_seg(i_seg, to_image) - new_seg_list.append(parsed_seg) - return Seg(type="seglist", data=new_seg_list) - elif seg_data.type == "image": - image_url = seg_data.data - try: - encoded_image = await get_image_base64(image_url) - except Exception as e: - logger.error(f"图片处理失败: {str(e)}") - return Seg(type="text", data="[图片]") - return Seg(type="image", data=encoded_image) - elif seg_data.type == "emoji": - image_url = seg_data.data - try: - encoded_image = await get_image_base64(image_url) - except Exception as e: - logger.error(f"图片处理失败: {str(e)}") - return Seg(type="text", data="[表情包]") - return Seg(type="emoji", data=encoded_image) - else: - logger.debug(f"不处理类型: {seg_data.type}") - return seg_data - else: - if seg_data.type == "seglist": - new_seg_list = [] - for i_seg in seg_data.data: - parsed_seg = await self._recursive_parse_image_seg(i_seg, to_image) - new_seg_list.append(parsed_seg) - return Seg(type="seglist", data=new_seg_list) - elif seg_data.type == "image": - return Seg(type="text", data="[图片]") - elif seg_data.type == "emoji": - return Seg(type="text", data="[动画表情]") - else: - logger.debug(f"不处理类型: {seg_data.type}") - return seg_data - - async def _handle_forward_message(self, message_list: list, layer: int) -> Tuple[Seg, int] | Tuple[None, int]: - # sourcery skip: low-code-quality - """ - 递归处理实际转发消息 - Parameters: - message_list: list: 转发消息列表,首层对应messages字段,后面对应content字段 - layer: int: 当前层级 - Returns: - seg_data: Seg: 处理后的消息段 - image_count: int: 图片数量 - """ - seg_list: List[Seg] = [] - image_count = 0 - if message_list is None: - return None, 0 - for sub_message in message_list: - sub_message: dict - sender_info: dict = sub_message.get("sender") - user_nickname: str = sender_info.get("nickname", "QQ用户") - user_nickname_str = f"【{user_nickname}】:" - break_seg = Seg(type="text", data="\n") - message_of_sub_message_list: List[Dict[str, Any]] = sub_message.get("message") - if not message_of_sub_message_list: - logger.warning("转发消息内容为空") - continue - message_of_sub_message = message_of_sub_message_list[0] - if message_of_sub_message.get("type") == RealMessageType.forward: - if layer >= 3: - full_seg_data = Seg( - type="text", - data=("--" * layer) + f"【{user_nickname}】:【转发消息】\n", - ) - else: - sub_message_data = message_of_sub_message.get("data") - if not sub_message_data: - continue - contents = sub_message_data.get("content") - seg_data, count = await self._handle_forward_message(contents, layer + 1) - image_count += count - head_tip = Seg( - type="text", - data=("--" * layer) + f"【{user_nickname}】: 合并转发消息内容:\n", - ) - full_seg_data = Seg(type="seglist", data=[head_tip, seg_data]) - seg_list.append(full_seg_data) - elif message_of_sub_message.get("type") == RealMessageType.text: - sub_message_data = message_of_sub_message.get("data") - if not sub_message_data: - continue - text_message = sub_message_data.get("text") - seg_data = Seg(type="text", data=text_message) - data_list: List[Any] = [] - if layer > 0: - data_list = [ - Seg(type="text", data=("--" * layer) + user_nickname_str), - seg_data, - break_seg, - ] - else: - data_list = [ - Seg(type="text", data=user_nickname_str), - seg_data, - break_seg, - ] - seg_list.append(Seg(type="seglist", data=data_list)) - elif message_of_sub_message.get("type") == RealMessageType.image: - image_count += 1 - image_data = message_of_sub_message.get("data") - sub_type = image_data.get("sub_type") - image_url = image_data.get("url") - data_list: List[Any] = [] - if sub_type == 0: - seg_data = Seg(type="image", data=image_url) - else: - seg_data = Seg(type="emoji", data=image_url) - if layer > 0: - data_list = [ - Seg(type="text", data=("--" * layer) + user_nickname_str), - seg_data, - break_seg, - ] - else: - data_list = [ - Seg(type="text", data=user_nickname_str), - seg_data, - break_seg, - ] - full_seg_data = Seg(type="seglist", data=data_list) - seg_list.append(full_seg_data) - return Seg(type="seglist", data=seg_list), image_count - - async def _get_forward_message(self, raw_message: dict) -> Dict[str, Any] | None: - forward_message_data: Dict = raw_message.get("data") - if not forward_message_data: - logger.warning("转发消息内容为空") - return None - forward_message_id = forward_message_data.get("id") - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps( - { - "action": "get_forward_msg", - "params": {"message_id": forward_message_id}, - "echo": request_uuid, - } - ).decode('utf-8') - try: - connection = self.get_server_connection() - if not connection: - logger.error("没有可用的 WebSocket 连接") - return None - await connection.send(payload) - response: dict = await get_response(request_uuid) - except TimeoutError: - logger.error("获取转发消息超时") - return None - except Exception as e: - logger.error(f"获取转发消息失败: {str(e)}") - return None - logger.debug( - f"转发消息原始格式:{orjson.dumps(response).decode('utf-8')[:80]}..." - if len(orjson.dumps(response).decode('utf-8')) > 80 - else orjson.dumps(response).decode('utf-8') - ) - response_data: Dict = response.get("data") - if not response_data: - logger.warning("转发消息内容为空或获取失败") - return None - return response_data.get("messages") - - # 消息缓冲功能已移除 - - -message_handler = MessageHandler() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py deleted file mode 100644 index b64db620e..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py +++ /dev/null @@ -1,120 +0,0 @@ -import asyncio - -from maim_message import MessageBase, Router - -from src.common.logger import get_logger -from src.plugin_system.apis import config_api - -from ..message_chunker import chunker - -logger = get_logger("napcat_adapter") - - -class MessageSending: - """ - 负责把消息发送到麦麦 - """ - - maibot_router: Router = None - plugin_config = None - _connection_retries = 0 - _max_retries = 3 - - def __init__(self): - pass - - def set_plugin_config(self, plugin_config: dict): - """设置插件配置""" - self.plugin_config = plugin_config - - async def _attempt_reconnect(self): - """尝试重新连接MoFox-Bot router""" - if self._connection_retries < self._max_retries: - self._connection_retries += 1 - logger.warning(f"尝试重新连接MoFox-Bot router (第{self._connection_retries}次)") - try: - # 重新导入router - from ..mmc_com_layer import router - - self.maibot_router = router - if self.maibot_router is not None: - logger.info("MoFox-Bot router重连成功") - self._connection_retries = 0 # 重置重试计数 - return True - except Exception as e: - logger.error(f"重连失败: {e}") - else: - logger.error(f"已达到最大重连次数({self._max_retries}),停止重试") - return False - - async def message_send(self, message_base: MessageBase) -> bool: - """ - 发送消息(Ada -> MMC 方向,需要实现切片) - Parameters: - message_base: MessageBase: 消息基类,包含发送目标和消息内容等信息 - """ - try: - # 检查maibot_router是否已初始化 - if self.maibot_router is None: - logger.warning("MoFox-Bot router未初始化,尝试重新连接") - if not await self._attempt_reconnect(): - logger.error("MoFox-Bot router重连失败,无法发送消息") - logger.error("请检查与MoFox-Bot之间的连接") - return False - # 检查是否需要切片发送 - message_dict = message_base.to_dict() - - if chunker.should_chunk_message(message_dict): - logger.info("消息过大,进行切片发送到 MoFox-Bot") - - # 切片消息 - chunks = chunker.chunk_message(message_dict) - - # 逐个发送切片 - for i, chunk in enumerate(chunks): - logger.debug(f"发送切片 {i + 1}/{len(chunks)} 到 MoFox-Bot") - - # 获取对应的客户端并发送切片 - platform = message_base.message_info.platform - - # 再次检查router状态(防止运行时被重置) - if self.maibot_router is None or not hasattr(self.maibot_router, "clients"): - logger.warning("MoFox-Bot router连接已断开,尝试重新连接") - if not await self._attempt_reconnect(): - logger.error("MoFox-Bot router重连失败,切片发送中止") - return False - - if platform not in self.maibot_router.clients: - logger.error(f"平台 {platform} 未连接") - return False - - client = self.maibot_router.clients[platform] - send_status = await client.send_message(chunk) - - if not send_status: - logger.error(f"发送切片 {i + 1}/{len(chunks)} 失败") - return False - - # 使用配置中的延迟时间 - if i < len(chunks) - 1 and self.plugin_config: - delay_ms = config_api.get_plugin_config(self.plugin_config, "slicing.delay_ms", 10) - delay_seconds = delay_ms / 1000.0 - logger.debug(f"切片发送延迟: {delay_ms}毫秒") - await asyncio.sleep(delay_seconds) - - logger.debug("所有切片发送完成") - return True - else: - # 直接发送小消息 - send_status = await self.maibot_router.send_message(message_base) - if not send_status: - raise RuntimeError("可能是路由未正确配置或连接异常") - return send_status - - except Exception as e: - logger.error(f"发送消息失败: {str(e)}") - logger.error("请检查与MoFox-Bot之间的连接") - return False - - -message_send_instance = MessageSending() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py deleted file mode 100644 index 12998239c..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py +++ /dev/null @@ -1,65 +0,0 @@ -import asyncio -import time - -from src.common.logger import get_logger -from src.plugin_system.apis import config_api - -from . import MetaEventType - -logger = get_logger("napcat_adapter") - - -class MetaEventHandler: - """ - 处理Meta事件 - """ - - def __init__(self): - self.interval = 5.0 # 默认值,稍后通过set_plugin_config设置 - self._interval_checking = False - self.plugin_config = None - - def set_plugin_config(self, plugin_config: dict): - """设置插件配置""" - self.plugin_config = plugin_config - # 更新interval值 - self.interval = ( - config_api.get_plugin_config(self.plugin_config, "napcat_server.heartbeat_interval", 5000) / 1000 - ) - - async def handle_meta_event(self, message: dict) -> None: - event_type = message.get("meta_event_type") - if event_type == MetaEventType.lifecycle: - sub_type = message.get("sub_type") - if sub_type == MetaEventType.Lifecycle.connect: - self_id = message.get("self_id") - self.last_heart_beat = time.time() - logger.info(f"Bot {self_id} 连接成功") - # 不在连接时立即启动心跳检查,等第一个心跳包到达后再启动 - elif event_type == MetaEventType.heartbeat: - if message["status"].get("online") and message["status"].get("good"): - self_id = message.get("self_id") - if not self._interval_checking and self_id: - # 第一次收到心跳包时才启动心跳检查 - asyncio.create_task(self.check_heartbeat(self_id)) - self.last_heart_beat = time.time() - interval = message.get("interval") - if interval: - self.interval = interval / 1000 - else: - self_id = message.get("self_id") - logger.warning(f"Bot {self_id} Napcat 端异常!") - - async def check_heartbeat(self, id: int) -> None: - self._interval_checking = True - while True: - now_time = time.time() - if now_time - self.last_heart_beat > self.interval * 2: - logger.error(f"Bot {id} 可能发生了连接断开,被下线,或者Napcat卡死!") - break - else: - logger.debug("心跳正常") - await asyncio.sleep(self.interval) - - -meta_event_handler = MetaEventHandler() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py deleted file mode 100644 index 1f6bf104e..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py +++ /dev/null @@ -1,705 +0,0 @@ -import asyncio -import orjson -import time -from typing import ClassVar, Optional, Tuple - -import websockets as Server -from maim_message import BaseMessageInfo, FormatInfo, GroupInfo, MessageBase, Seg, UserInfo - -from src.common.logger import get_logger -from src.plugin_system.apis import config_api - -from ...CONSTS import PLUGIN_NAME, QQ_FACE -from ..database import BanUser, is_identical, napcat_db -from ..utils import ( - get_group_info, - get_member_info, - get_self_info, - get_stranger_info, - read_ban_list, -) -from ..websocket_manager import websocket_manager -from . import ACCEPT_FORMAT, NoticeType -from .message_handler import message_handler -from .message_sending import message_send_instance - -logger = get_logger("napcat_adapter") - -notice_queue: asyncio.Queue[MessageBase] = asyncio.Queue(maxsize=100) -unsuccessful_notice_queue: asyncio.Queue[MessageBase] = asyncio.Queue(maxsize=3) - - -class NoticeHandler: - banned_list: ClassVar[list[BanUser]] = [] # 当前仍在禁言中的用户列表 - lifted_list: ClassVar[list[BanUser]] = [] # 已经自然解除禁言 - - def __init__(self): - self.server_connection: Server.ServerConnection | None = None - self.last_poke_time: float = 0.0 # 记录最后一次针对机器人的戳一戳时间 - self.plugin_config = None - - def set_plugin_config(self, plugin_config: dict): - """设置插件配置""" - self.plugin_config = plugin_config - - async def set_server_connection(self, server_connection: Server.ServerConnection) -> None: - """设置Napcat连接""" - self.server_connection = server_connection - - while self.server_connection.state != Server.State.OPEN: - await asyncio.sleep(0.5) - self.banned_list, self.lifted_list = await read_ban_list(self.server_connection) - - asyncio.create_task(self.auto_lift_detect()) - asyncio.create_task(self.send_notice()) - asyncio.create_task(self.handle_natural_lift()) - - def get_server_connection(self) -> Server.ServerConnection: - """获取当前的服务器连接""" - # 优先使用直接设置的连接,否则从 websocket_manager 获取 - if self.server_connection: - return self.server_connection - return websocket_manager.get_connection() - - async def _ban_operation(self, group_id: int, user_id: Optional[int] = None, lift_time: Optional[int] = None) -> None: - """ - 将用户禁言记录添加到self.banned_list中 - 如果是全体禁言,则user_id为0 - """ - if user_id is None: - user_id = 0 # 使用0表示全体禁言 - lift_time = -1 - ban_record = BanUser(user_id=user_id, group_id=group_id, lift_time=lift_time) - for record in list(self.banned_list): - if is_identical(record, ban_record): - self.banned_list.remove(record) - self.banned_list.append(ban_record) - await napcat_db.create_ban_record(ban_record) # 更新 - return - self.banned_list.append(ban_record) - await napcat_db.create_ban_record(ban_record) # 新建 - - async def _lift_operation(self, group_id: int, user_id: Optional[int] = None) -> None: - """ - 从self.lifted_group_list中移除已经解除全体禁言的群 - """ - if user_id is None: - user_id = 0 # 使用0表示全体禁言 - ban_record = BanUser(user_id=user_id, group_id=group_id, lift_time=-1) - self.lifted_list.append(ban_record) - # 从被禁言列表里移除对应记录 - for record in list(self.banned_list): - if is_identical(record, ban_record): - self.banned_list.remove(record) - break - await napcat_db.delete_ban_record(ban_record) - - async def handle_notice(self, raw_message: dict) -> None: - notice_type = raw_message.get("notice_type") - # message_time: int = raw_message.get("time") - message_time: float = time.time() # 应可乐要求,现在是float了 - - self_id = raw_message.get("self_id") - group_id = raw_message.get("group_id") - user_id = raw_message.get("user_id") - target_id = raw_message.get("target_id") - - handled_message: Seg = None - user_info: UserInfo = None - system_notice: bool = False - - match notice_type: - case NoticeType.friend_recall: - logger.info("好友撤回一条消息") - logger.info(f"撤回消息ID:{raw_message.get('message_id')}, 撤回时间:{raw_message.get('time')}") - logger.warning("暂时不支持撤回消息处理") - case NoticeType.group_recall: - logger.info("群内用户撤回一条消息") - logger.info(f"撤回消息ID:{raw_message.get('message_id')}, 撤回时间:{raw_message.get('time')}") - logger.warning("暂时不支持撤回消息处理") - case NoticeType.notify: - sub_type = raw_message.get("sub_type") - match sub_type: - case NoticeType.Notify.poke: - if config_api.get_plugin_config(self.plugin_config, "features.enable_poke", True) and await message_handler.check_allow_to_chat( - user_id, group_id, False, False - ): - logger.debug("处理戳一戳消息") - handled_message, user_info = await self.handle_poke_notify(raw_message, group_id, user_id) - else: - logger.warning("戳一戳消息被禁用,取消戳一戳处理") - case NoticeType.Notify.input_status: - from src.plugin_system.core.event_manager import event_manager - - from ...event_types import NapcatEvent - - event_manager.emit_event(NapcatEvent.ON_RECEIVED.FRIEND_INPUT, permission_group=PLUGIN_NAME) - case _: - logger.warning(f"不支持的notify类型: {notice_type}.{sub_type}") - case NoticeType.group_msg_emoji_like: - # 该事件转移到 handle_group_emoji_like_notify函数内触发 - if config_api.get_plugin_config(self.plugin_config, "features.enable_emoji_like", True): - logger.debug("处理群聊表情回复") - handled_message, user_info = await self.handle_group_emoji_like_notify(raw_message,group_id,user_id) - else: - logger.warning("群聊表情回复被禁用,取消群聊表情回复处理") - case NoticeType.group_ban: - sub_type = raw_message.get("sub_type") - match sub_type: - case NoticeType.GroupBan.ban: - if not await message_handler.check_allow_to_chat(user_id, group_id, True, False): - return None - logger.info("处理群禁言") - handled_message, user_info = await self.handle_ban_notify(raw_message, group_id) - system_notice = True - case NoticeType.GroupBan.lift_ban: - if not await message_handler.check_allow_to_chat(user_id, group_id, True, False): - return None - logger.info("处理解除群禁言") - handled_message, user_info = await self.handle_lift_ban_notify(raw_message, group_id) - system_notice = True - case _: - logger.warning(f"不支持的group_ban类型: {notice_type}.{sub_type}") - case NoticeType.group_upload: - logger.info("群文件上传") - if user_id == self_id: - logger.info("检测到机器人自己上传文件,忽略此通知") - return None - if not await message_handler.check_allow_to_chat(user_id, group_id, False, False): - return None - handled_message, user_info = await self.handle_group_upload_notify(raw_message, group_id, user_id, self_id) - case _: - logger.warning(f"不支持的notice类型: {notice_type}") - return None - if not handled_message or not user_info: - logger.warning("notice处理失败或不支持") - return None - - group_info: GroupInfo | None = None - if group_id: - fetched_group_info = await get_group_info(self.get_server_connection(), group_id) - group_name: str | None = None - if fetched_group_info: - group_name = fetched_group_info.get("group_name") - else: - logger.warning("无法获取notice消息所在群的名称") - group_info = GroupInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - group_id=group_id, - group_name=group_name, - ) - - # 准备additional_config,包含notice标志 - notice_config = { - "is_notice": system_notice, # 禁言/解禁是系统通知 - "is_public_notice": False, # 群内notice,非公共 - "target_id": target_id, # 在这里塞了一个target_id,方便mmc那边知道被戳的人是谁 - } - - # 根据notice_type设置notice_type字段 - if system_notice: - sub_type = raw_message.get("sub_type") - if notice_type == NoticeType.group_ban: - if sub_type == NoticeType.GroupBan.ban: - user_id_in_ban = raw_message.get("user_id") - if user_id_in_ban == 0: - notice_config["notice_type"] = "group_whole_ban" - else: - notice_config["notice_type"] = "group_ban" - elif sub_type == NoticeType.GroupBan.lift_ban: - user_id_in_ban = raw_message.get("user_id") - if user_id_in_ban == 0: - notice_config["notice_type"] = "group_whole_lift_ban" - else: - notice_config["notice_type"] = "group_lift_ban" - elif notice_type == NoticeType.notify: - sub_type = raw_message.get("sub_type") - if sub_type == NoticeType.Notify.poke: - notice_config["notice_type"] = "poke" - notice_config["is_notice"] = True # 戳一戳也是notice - elif notice_type == NoticeType.group_msg_emoji_like: - notice_config["notice_type"] = "emoji_like" - notice_config["is_notice"] = True # 表情回复也是notice - elif notice_type == NoticeType.group_upload: - notice_config["notice_type"] = "group_upload" - notice_config["is_notice"] = True # 文件上传也是notice - - message_info: BaseMessageInfo = BaseMessageInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - message_id="notice", - time=message_time, - user_info=user_info, - group_info=group_info, - template_info=None, - format_info=FormatInfo( - content_format=["text", "notify"], - accept_format=ACCEPT_FORMAT, - ), - additional_config=notice_config, # 字典而不是JSON字符串 - ) - - message_base: MessageBase = MessageBase( - message_info=message_info, - message_segment=handled_message, - raw_message=orjson.dumps(raw_message).decode('utf-8'), - ) - - if system_notice: - await self.put_notice(message_base) - return None - else: - logger.debug("发送到Maibot处理通知信息") - await message_send_instance.message_send(message_base) - return None - - async def handle_poke_notify( - self, raw_message: dict, group_id: int, user_id: int - ) -> Tuple[Seg | None, UserInfo | None]: - # sourcery skip: merge-comparisons, merge-duplicate-blocks, remove-redundant-if, remove-unnecessary-else, swap-if-else-branches - self_info: dict = await get_self_info(self.get_server_connection()) - - if not self_info: - logger.error("自身信息获取失败") - return None, None - - self_id = raw_message.get("self_id") - target_id = raw_message.get("target_id") - - # 防抖检查:如果是针对机器人的戳一戳,检查防抖时间 - if self_id == target_id: - current_time = time.time() - debounce_seconds = config_api.get_plugin_config(self.plugin_config, "features.poke_debounce_seconds", 2.0) - - if self.last_poke_time > 0: - time_diff = current_time - self.last_poke_time - if time_diff < debounce_seconds: - logger.debug(f"戳一戳防抖:用户 {user_id} 的戳一戳被忽略(距离上次戳一戳 {time_diff:.2f} 秒)") - return None, None - - # 记录这次戳一戳的时间 - self.last_poke_time = current_time - - target_name: str = None - raw_info: list = raw_message.get("raw_info") - - if group_id: - user_qq_info: dict = await get_member_info(self.get_server_connection(), group_id, user_id) - else: - user_qq_info: dict = await get_stranger_info(self.get_server_connection(), user_id) - if user_qq_info: - user_name = user_qq_info.get("nickname") - user_cardname = user_qq_info.get("card") - else: - user_name = "QQ用户" - user_cardname = "QQ用户" - logger.debug("无法获取戳一戳对方的用户昵称") - - # 计算Seg - if self_id == target_id: - display_name = "" - target_name = self_info.get("nickname") - - elif self_id == user_id: - # 让ada不发送麦麦戳别人的消息 - return None, None - - else: - # 如果配置为忽略不是针对自己的戳一戳,则直接返回None - if config_api.get_plugin_config(self.plugin_config, "features.ignore_non_self_poke", False): - logger.debug("忽略不是针对自己的戳一戳消息") - return None, None - - # 老实说这一步判定没啥意义,毕竟私聊是没有其他人之间的戳一戳,但是感觉可以有这个判定来强限制群聊环境 - if group_id: - fetched_member_info: dict = await get_member_info(self.get_server_connection(), group_id, target_id) - if fetched_member_info: - target_name = fetched_member_info.get("nickname") - else: - target_name = "QQ用户" - logger.debug("无法获取被戳一戳方的用户昵称") - display_name = user_name - else: - return None, None - - first_txt: str = "戳了戳" - second_txt: str = "" - try: - first_txt = raw_info[2].get("txt", "戳了戳") - second_txt = raw_info[4].get("txt", "") - except Exception as e: - logger.warning(f"解析戳一戳消息失败: {str(e)},将使用默认文本") - - user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - user_id=user_id, - user_nickname=user_name, - user_cardname=user_cardname, - ) - - seg_data: Seg = Seg( - type="text", - data=f"{display_name}{first_txt}{target_name}{second_txt}(这是QQ的一个功能,用于提及某人,但没那么明显)", - ) - return seg_data, user_info - - async def handle_group_emoji_like_notify(self, raw_message: dict, group_id: int, user_id: int): - if not group_id: - logger.error("群ID不能为空,无法处理群聊表情回复通知") - return None, None - - user_qq_info: dict = await get_member_info(self.get_server_connection(), group_id, user_id) - if user_qq_info: - user_name = user_qq_info.get("nickname") - user_cardname = user_qq_info.get("card") - else: - user_name = "QQ用户" - user_cardname = "QQ用户" - logger.debug("无法获取表情回复对方的用户昵称") - - from src.plugin_system.core.event_manager import event_manager - - from ...event_types import NapcatEvent - - target_message = await event_manager.trigger_event(NapcatEvent.MESSAGE.GET_MSG,message_id=raw_message.get("message_id","")) - target_message_text = target_message.get_message_result().get("data",{}).get("raw_message","") - if not target_message: - logger.error("未找到对应消息") - return None, None - if len(target_message_text) > 15: - target_message_text = target_message_text[:15] + "..." - - user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - user_id=user_id, - user_nickname=user_name, - user_cardname=user_cardname, - ) - - like_emoji_id = raw_message.get("likes")[0].get("emoji_id") - event_manager.emit_event( - NapcatEvent.ON_RECEIVED.EMOJI_LIEK, - permission_group=PLUGIN_NAME, - group_id=group_id, - user_id=user_id, - message_id=raw_message.get("message_id",""), - emoji_id=like_emoji_id - ) - seg_data = Seg(type="text",data=f"{user_name}使用Emoji表情{QQ_FACE.get(like_emoji_id,"")}回复了你的消息[{target_message_text}]") - return seg_data, user_info - - async def handle_group_upload_notify(self, raw_message: dict, group_id: int, user_id: int, self_id: int): - if not group_id: - logger.error("群ID不能为空,无法处理群文件上传通知") - return None, None - - user_qq_info: dict = await get_member_info(self.get_server_connection(), group_id, user_id) - if user_qq_info: - user_name = user_qq_info.get("nickname") - user_cardname = user_qq_info.get("card") - else: - user_name = "QQ用户" - user_cardname = "QQ用户" - logger.debug("无法获取上传文件的用户昵称") - - file_info = raw_message.get("file") - if not file_info: - logger.error("群文件上传通知中缺少文件信息") - return None, None - - user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - user_id=user_id, - user_nickname=user_name, - user_cardname=user_cardname, - ) - - file_name = file_info.get("name", "未知文件") - file_size = file_info.get("size", 0) - - seg_data = Seg(type="text", data=f"{user_name} 上传了文件: {file_name} (大小: {file_size} 字节)") - return seg_data, user_info - - async def handle_ban_notify(self, raw_message: dict, group_id: int) -> Tuple[Seg, UserInfo] | Tuple[None, None]: - if not group_id: - logger.error("群ID不能为空,无法处理禁言通知") - return None, None - - # 计算user_info - operator_id = raw_message.get("operator_id") - operator_nickname: str = None - operator_cardname: str = None - - member_info: dict = await get_member_info(self.get_server_connection(), group_id, operator_id) - if member_info: - operator_nickname = member_info.get("nickname") - operator_cardname = member_info.get("card") - else: - logger.warning("无法获取禁言执行者的昵称,消息可能会无效") - operator_nickname = "QQ用户" - - operator_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - user_id=operator_id, - user_nickname=operator_nickname, - user_cardname=operator_cardname, - ) - - # 计算Seg - user_id = raw_message.get("user_id") - banned_user_info: UserInfo = None - user_nickname: str = "QQ用户" - user_cardname: str = None - sub_type: str = None - - duration = raw_message.get("duration") - if duration is None: - logger.error("禁言时长不能为空,无法处理禁言通知") - return None, None - - if user_id == 0: # 为全体禁言 - sub_type: str = "whole_ban" - await self._ban_operation(group_id) - else: # 为单人禁言 - # 获取被禁言人的信息 - sub_type: str = "ban" - fetched_member_info: dict = await get_member_info(self.get_server_connection(), group_id, user_id) - if fetched_member_info: - user_nickname = fetched_member_info.get("nickname") - user_cardname = fetched_member_info.get("card") - banned_user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - user_id=user_id, - user_nickname=user_nickname, - user_cardname=user_cardname, - ) - await self._ban_operation(group_id, user_id, int(time.time() + duration)) - - seg_data: Seg = Seg( - type="notify", - data={ - "sub_type": sub_type, - "duration": duration, - "banned_user_info": banned_user_info.to_dict() if banned_user_info else None, - }, - ) - - return seg_data, operator_info - - async def handle_lift_ban_notify( - self, raw_message: dict, group_id: int - ) -> Tuple[Seg, UserInfo] | Tuple[None, None]: - if not group_id: - logger.error("群ID不能为空,无法处理解除禁言通知") - return None, None - - # 计算user_info - operator_id = raw_message.get("operator_id") - operator_nickname: str = None - operator_cardname: str = None - - member_info: dict = await get_member_info(self.get_server_connection(), group_id, operator_id) - if member_info: - operator_nickname = member_info.get("nickname") - operator_cardname = member_info.get("card") - else: - logger.warning("无法获取解除禁言执行者的昵称,消息可能会无效") - operator_nickname = "QQ用户" - - operator_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - user_id=operator_id, - user_nickname=operator_nickname, - user_cardname=operator_cardname, - ) - - # 计算Seg - sub_type: str = None - user_nickname: str = "QQ用户" - user_cardname: str = None - lifted_user_info: UserInfo = None - - user_id = raw_message.get("user_id") - if user_id == 0: # 全体禁言解除 - sub_type = "whole_lift_ban" - await self._lift_operation(group_id) - else: # 单人禁言解除 - sub_type = "lift_ban" - # 获取被解除禁言人的信息 - fetched_member_info: dict = await get_member_info(self.get_server_connection(), group_id, user_id) - if fetched_member_info: - user_nickname = fetched_member_info.get("nickname") - user_cardname = fetched_member_info.get("card") - else: - logger.warning("无法获取解除禁言消息发送者的昵称,消息可能会无效") - lifted_user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - user_id=user_id, - user_nickname=user_nickname, - user_cardname=user_cardname, - ) - await self._lift_operation(group_id, user_id) - - seg_data: Seg = Seg( - type="notify", - data={ - "sub_type": sub_type, - "lifted_user_info": lifted_user_info.to_dict() if lifted_user_info else None, - }, - ) - return seg_data, operator_info - - @staticmethod - async def put_notice(message_base: MessageBase) -> None: - """ - 将处理后的通知消息放入通知队列 - """ - if notice_queue.full() or unsuccessful_notice_queue.full(): - logger.warning("通知队列已满,可能是多次发送失败,消息丢弃") - else: - await notice_queue.put(message_base) - - async def handle_natural_lift(self) -> None: - while True: - if len(self.lifted_list) != 0: - lift_record = self.lifted_list.pop() - group_id = lift_record.group_id - user_id = lift_record.user_id - - asyncio.create_task(napcat_db.delete_ban_record(lift_record)) # 从数据库中删除禁言记录 - - seg_message: Seg = await self.natural_lift(group_id, user_id) - - fetched_group_info = await get_group_info(self.get_server_connection(), group_id) - group_name: str = None - if fetched_group_info: - group_name = fetched_group_info.get("group_name") - else: - logger.warning("无法获取notice消息所在群的名称") - group_info = GroupInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - group_id=group_id, - group_name=group_name, - ) - - # 准备notice标志 - notice_config = { - "is_notice": True, - "is_public_notice": False, - "notice_type": "group_lift_ban" if user_id != 0 else "group_whole_lift_ban", - } - - message_info: BaseMessageInfo = BaseMessageInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - message_id="notice", - time=time.time(), - user_info=None, # 自然解除禁言没有操作者 - group_info=group_info, - template_info=None, - format_info=None, - additional_config=notice_config, # 字典而不是JSON字符串 - ) - - message_base: MessageBase = MessageBase( - message_info=message_info, - message_segment=seg_message, - raw_message=orjson.dumps( - { - "post_type": "notice", - "notice_type": "group_ban", - "sub_type": "lift_ban", - "group_id": group_id, - "user_id": user_id, - "operator_id": None, # 自然解除禁言没有操作者 - } - ).decode('utf-8'), - ) - - await self.put_notice(message_base) - await asyncio.sleep(0.5) # 确保队列处理间隔 - else: - await asyncio.sleep(5) # 每5秒检查一次 - - async def natural_lift(self, group_id: int, user_id: int) -> Seg | None: - if not group_id: - logger.error("群ID不能为空,无法处理解除禁言通知") - return None - - if user_id == 0: # 理论上永远不会触发 - return Seg( - type="notify", - data={ - "sub_type": "whole_lift_ban", - "lifted_user_info": None, - }, - ) - - user_nickname: str = "QQ用户" - user_cardname: str = None - fetched_member_info: dict = await get_member_info(self.get_server_connection(), group_id, user_id) - if fetched_member_info: - user_nickname = fetched_member_info.get("nickname") - user_cardname = fetched_member_info.get("card") - - lifted_user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), - user_id=user_id, - user_nickname=user_nickname, - user_cardname=user_cardname, - ) - - return Seg( - type="notify", - data={ - "sub_type": "lift_ban", - "lifted_user_info": lifted_user_info.to_dict(), - }, - ) - - async def auto_lift_detect(self) -> None: - while True: - if len(self.banned_list) == 0: - await asyncio.sleep(5) - continue - for ban_record in self.banned_list: - if ban_record.user_id == 0 or ban_record.lift_time == -1: - continue - if ban_record.lift_time <= int(time.time()): - # 触发自然解除禁言 - logger.debug(f"检测到用户 {ban_record.user_id} 在群 {ban_record.group_id} 的禁言已解除") - self.lifted_list.append(ban_record) - self.banned_list.remove(ban_record) - await asyncio.sleep(5) - - @staticmethod - async def send_notice() -> None: - """ - 发送通知消息到Napcat - """ - while True: - if not unsuccessful_notice_queue.empty(): - to_be_send: MessageBase = await unsuccessful_notice_queue.get() - try: - send_status = await message_send_instance.message_send(to_be_send) - if send_status: - unsuccessful_notice_queue.task_done() - else: - await unsuccessful_notice_queue.put(to_be_send) - except Exception as e: - logger.error(f"发送通知消息失败: {str(e)}") - await unsuccessful_notice_queue.put(to_be_send) - await asyncio.sleep(1) - continue - to_be_send: MessageBase = await notice_queue.get() - try: - send_status = await message_send_instance.message_send(to_be_send) - if send_status: - notice_queue.task_done() - else: - await unsuccessful_notice_queue.put(to_be_send) - except Exception as e: - logger.error(f"发送通知消息失败: {str(e)}") - await unsuccessful_notice_queue.put(to_be_send) - await asyncio.sleep(1) - - -notice_handler = NoticeHandler() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py deleted file mode 100644 index 3d1dd191a..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py +++ /dev/null @@ -1,252 +0,0 @@ -qq_face: dict = { - "0": "[表情:惊讶]", - "1": "[表情:撇嘴]", - "2": "[表情:色]", - "3": "[表情:发呆]", - "4": "[表情:得意]", - "5": "[表情:流泪]", - "6": "[表情:害羞]", - "7": "[表情:闭嘴]", - "8": "[表情:睡]", - "9": "[表情:大哭]", - "10": "[表情:尴尬]", - "11": "[表情:发怒]", - "12": "[表情:调皮]", - "13": "[表情:呲牙]", - "14": "[表情:微笑]", - "15": "[表情:难过]", - "16": "[表情:酷]", - "18": "[表情:抓狂]", - "19": "[表情:吐]", - "20": "[表情:偷笑]", - "21": "[表情:可爱]", - "22": "[表情:白眼]", - "23": "[表情:傲慢]", - "24": "[表情:饥饿]", - "25": "[表情:困]", - "26": "[表情:惊恐]", - "27": "[表情:流汗]", - "28": "[表情:憨笑]", - "29": "[表情:悠闲]", - "30": "[表情:奋斗]", - "31": "[表情:咒骂]", - "32": "[表情:疑问]", - "33": "[表情: 嘘]", - "34": "[表情:晕]", - "35": "[表情:折磨]", - "36": "[表情:衰]", - "37": "[表情:骷髅]", - "38": "[表情:敲打]", - "39": "[表情:再见]", - "41": "[表情:发抖]", - "42": "[表情:爱情]", - "43": "[表情:跳跳]", - "46": "[表情:猪头]", - "49": "[表情:拥抱]", - "53": "[表情:蛋糕]", - "56": "[表情:刀]", - "59": "[表情:便便]", - "60": "[表情:咖啡]", - "63": "[表情:玫瑰]", - "64": "[表情:凋谢]", - "66": "[表情:爱心]", - "67": "[表情:心碎]", - "74": "[表情:太阳]", - "75": "[表情:月亮]", - "76": "[表情:赞]", - "77": "[表情:踩]", - "78": "[表情:握手]", - "79": "[表情:胜利]", - "85": "[表情:飞吻]", - "86": "[表情:怄火]", - "89": "[表情:西瓜]", - "96": "[表情:冷汗]", - "97": "[表情:擦汗]", - "98": "[表情:抠鼻]", - "99": "[表情:鼓掌]", - "100": "[表情:糗大了]", - "101": "[表情:坏笑]", - "102": "[表情:左哼哼]", - "103": "[表情:右哼哼]", - "104": "[表情:哈欠]", - "105": "[表情:鄙视]", - "106": "[表情:委屈]", - "107": "[表情:快哭了]", - "108": "[表情:阴险]", - "109": "[表情:左亲亲]", - "110": "[表情:吓]", - "111": "[表情:可怜]", - "112": "[表情:菜刀]", - "114": "[表情:篮球]", - "116": "[表情:示爱]", - "118": "[表情:抱拳]", - "119": "[表情:勾引]", - "120": "[表情:拳头]", - "121": "[表情:差劲]", - "123": "[表情:NO]", - "124": "[表情:OK]", - "125": "[表情:转圈]", - "129": "[表情:挥手]", - "137": "[表情:鞭炮]", - "144": "[表情:喝彩]", - "146": "[表情:爆筋]", - "147": "[表情:棒棒糖]", - "169": "[表情:手枪]", - "171": "[表情:茶]", - "172": "[表情:眨眼睛]", - "173": "[表情:泪奔]", - "174": "[表情:无奈]", - "175": "[表情:卖萌]", - "176": "[表情:小纠结]", - "177": "[表情:喷血]", - "178": "[表情:斜眼笑]", - "179": "[表情:doge]", - "181": "[表情:戳一戳]", - "182": "[表情:笑哭]", - "183": "[表情:我最美]", - "185": "[表情:羊驼]", - "187": "[表情:幽灵]", - "201": "[表情:点赞]", - "212": "[表情:托腮]", - "262": "[表情:脑阔疼]", - "263": "[表情:沧桑]", - "264": "[表情:捂脸]", - "265": "[表情:辣眼睛]", - "266": "[表情:哦哟]", - "267": "[表情:头秃]", - "268": "[表情:问号脸]", - "269": "[表情:暗中观察]", - "270": "[表情:emm]", - "271": "[表情:吃 瓜]", - "272": "[表情:呵呵哒]", - "273": "[表情:我酸了]", - "277": "[表情:滑稽狗头]", - "281": "[表情:翻白眼]", - "282": "[表情:敬礼]", - "283": "[表情:狂笑]", - "284": "[表情:面无表情]", - "285": "[表情:摸鱼]", - "286": "[表情:魔鬼笑]", - "287": "[表情:哦]", - "289": "[表情:睁眼]", - "293": "[表情:摸锦鲤]", - "294": "[表情:期待]", - "295": "[表情:拿到红包]", - "297": "[表情:拜谢]", - "298": "[表情:元宝]", - "299": "[表情:牛啊]", - "300": "[表情:胖三斤]", - "302": "[表情:左拜年]", - "303": "[表情:右拜年]", - "305": "[表情:右亲亲]", - "306": "[表情:牛气冲天]", - "307": "[表情:喵喵]", - "311": "[表情:打call]", - "312": "[表情:变形]", - "314": "[表情:仔细分析]", - "317": "[表情:菜汪]", - "318": "[表情:崇拜]", - "319": "[表情: 比心]", - "320": "[表情:庆祝]", - "323": "[表情:嫌弃]", - "324": "[表情:吃糖]", - "325": "[表情:惊吓]", - "326": "[表情:生气]", - "332": "[表情:举牌牌]", - "333": "[表情:烟花]", - "334": "[表情:虎虎生威]", - "336": "[表情:豹富]", - "337": "[表情:花朵脸]", - "338": "[表情:我想开了]", - "339": "[表情:舔屏]", - "341": "[表情:打招呼]", - "342": "[表情:酸Q]", - "343": "[表情:我方了]", - "344": "[表情:大怨种]", - "345": "[表情:红包多多]", - "346": "[表情:你真棒棒]", - "347": "[表情:大展宏兔]", - "349": "[表情:坚强]", - "350": "[表情:贴贴]", - "351": "[表情:敲敲]", - "352": "[表情:咦]", - "353": "[表情:拜托]", - "354": "[表情:尊嘟假嘟]", - "355": "[表情:耶]", - "356": "[表情:666]", - "357": "[表情:裂开]", - "392": "[表情:龙年 快乐]", - "393": "[表情:新年中龙]", - "394": "[表情:新年大龙]", - "395": "[表情:略略略]", - "396": "[表情:龙年快乐]", - "424": "[表情:按钮]", - "😊": "[表情:嘿嘿]", - "😌": "[表情:羞涩]", - "😚": "[ 表情:亲亲]", - "😓": "[表情:汗]", - "😰": "[表情:紧张]", - "😝": "[表情:吐舌]", - "😁": "[表情:呲牙]", - "😜": "[表情:淘气]", - "☺": "[表情:可爱]", - "😍": "[表情:花痴]", - "😔": "[表情:失落]", - "😄": "[表情:高兴]", - "😏": "[表情:哼哼]", - "😒": "[表情:不屑]", - "😳": "[表情:瞪眼]", - "😘": "[表情:飞吻]", - "😭": "[表情:大哭]", - "😱": "[表情:害怕]", - "😂": "[表情:激动]", - "💪": "[表情:肌肉]", - "👊": "[表情:拳头]", - "👍": "[表情 :厉害]", - "👏": "[表情:鼓掌]", - "👎": "[表情:鄙视]", - "🙏": "[表情:合十]", - "👌": "[表情:好的]", - "👆": "[表情:向上]", - "👀": "[表情:眼睛]", - "🍜": "[表情:拉面]", - "🍧": "[表情:刨冰]", - "🍞": "[表情:面包]", - "🍺": "[表情:啤酒]", - "🍻": "[表情:干杯]", - "☕": "[表情:咖啡]", - "🍎": "[表情:苹果]", - "🍓": "[表情:草莓]", - "🍉": "[表情:西瓜]", - "🚬": "[表情:吸烟]", - "🌹": "[表情:玫瑰]", - "🎉": "[表情:庆祝]", - "💝": "[表情:礼物]", - "💣": "[表情:炸弹]", - "✨": "[表情:闪光]", - "💨": "[表情:吹气]", - "💦": "[表情:水]", - "🔥": "[表情:火]", - "💤": "[表情:睡觉]", - "💩": "[表情:便便]", - "💉": "[表情:打针]", - "📫": "[表情:邮箱]", - "🐎": "[表情:骑马]", - "👧": "[表情:女孩]", - "👦": "[表情:男孩]", - "🐵": "[表情:猴]", - "🐷": "[表情:猪]", - "🐮": "[表情:牛]", - "🐔": "[表情:公鸡]", - "🐸": "[表情:青蛙]", - "👻": "[表情:幽灵]", - "🐛": "[表情:虫]", - "🐶": "[表情:狗]", - "🐳": "[表情:鲸鱼]", - "👢": "[表情:靴子]", - "☀": "[表情:晴天]", - "❔": "[表情:问号]", - "🔫": "[表情:手枪]", - "💓": "[表情:爱 心]", - "🏪": "[表情:便利店]", -} diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py b/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py deleted file mode 100644 index 3458ad6d5..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py +++ /dev/null @@ -1,63 +0,0 @@ -import asyncio -import time -from typing import Dict - -from src.common.logger import get_logger -from src.plugin_system.apis import config_api - -logger = get_logger("napcat_adapter") - -response_dict: Dict = {} -response_time_dict: Dict = {} -plugin_config = None - - -def set_plugin_config(config: dict): - """设置插件配置""" - global plugin_config - plugin_config = config - - -async def get_response(request_id: str, timeout: int = 10) -> dict: - response = await asyncio.wait_for(_get_response(request_id), timeout) - _ = response_time_dict.pop(request_id) - logger.debug(f"响应信息id: {request_id} 已从响应字典中取出") - return response - - -async def _get_response(request_id: str) -> dict: - """ - 内部使用的获取响应函数,主要用于在需要时获取响应 - """ - while request_id not in response_dict: - await asyncio.sleep(0.2) - return response_dict.pop(request_id) - - -async def put_response(response: dict): - echo_id = response.get("echo") - now_time = time.time() - response_dict[echo_id] = response - response_time_dict[echo_id] = now_time - logger.debug(f"响应信息id: {echo_id} 已存入响应字典") - - -async def check_timeout_response() -> None: - while True: - cleaned_message_count: int = 0 - now_time = time.time() - - # 获取心跳间隔配置 - heartbeat_interval = 30 # 默认值 - if plugin_config: - heartbeat_interval = config_api.get_plugin_config(plugin_config, "napcat_server.heartbeat_interval", 30) - - for echo_id, response_time in list(response_time_dict.items()): - if now_time - response_time > heartbeat_interval: - cleaned_message_count += 1 - response_dict.pop(echo_id) - response_time_dict.pop(echo_id) - logger.warning(f"响应消息 {echo_id} 超时,已删除") - if cleaned_message_count > 0: - logger.info(f"已删除 {cleaned_message_count} 条超时响应消息") - await asyncio.sleep(heartbeat_interval) diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py deleted file mode 100644 index 9ec950bc8..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py +++ /dev/null @@ -1,705 +0,0 @@ -import orjson -import random -import time -import websockets as Server -import uuid -from maim_message import ( - UserInfo, - GroupInfo, - Seg, - BaseMessageInfo, - MessageBase, -) -from typing import Dict, Any, Tuple, Optional -from src.plugin_system.apis import config_api - -from . import CommandType -from .response_pool import get_response -from src.common.logger import get_logger - -logger = get_logger("napcat_adapter") -from .utils import get_image_format, convert_image_to_gif -from .recv_handler.message_sending import message_send_instance -from .websocket_manager import websocket_manager - - -class SendHandler: - def __init__(self): - self.server_connection: Optional[Server.ServerConnection] = None - self.plugin_config = None - - def set_plugin_config(self, plugin_config: dict): - """设置插件配置""" - self.plugin_config = plugin_config - - async def set_server_connection(self, server_connection: Server.ServerConnection) -> None: - """设置Napcat连接""" - self.server_connection = server_connection - - def get_server_connection(self) -> Optional[Server.ServerConnection]: - """获取当前的服务器连接""" - # 优先使用直接设置的连接,否则从 websocket_manager 获取 - if self.server_connection: - return self.server_connection - return websocket_manager.get_connection() - - async def handle_message(self, raw_message_base_dict: dict) -> None: - raw_message_base: MessageBase = MessageBase.from_dict(raw_message_base_dict) - message_segment: Seg = raw_message_base.message_segment - logger.info("接收到来自MoFox-Bot的消息,处理中") - if message_segment.type == "command": - logger.info("处理命令") - return await self.send_command(raw_message_base) - elif message_segment.type == "adapter_command": - logger.info("处理适配器命令") - return await self.handle_adapter_command(raw_message_base) - elif message_segment.type == "adapter_response": - # adapter_response消息是Napcat发送给Bot的,不应该在这里处理 - # 这个handler只处理Bot发送给Napcat的消息 - logger.info("收到adapter_response消息,此消息应该由Bot端处理,跳过") - return None - else: - logger.info("处理普通消息") - return await self.send_normal_message(raw_message_base) - - async def send_normal_message(self, raw_message_base: MessageBase) -> None: - """ - 处理普通消息发送 - """ - logger.info("处理普通信息中") - message_info: BaseMessageInfo = raw_message_base.message_info - message_segment: Seg = raw_message_base.message_segment - group_info: Optional[GroupInfo] = message_info.group_info - user_info: Optional[UserInfo] = message_info.user_info - target_id: Optional[int] = None - action: Optional[str] = None - id_name: Optional[str] = None - processed_message: list = [] - try: - if user_info: - processed_message = await self.handle_seg_recursive(message_segment, user_info) - except Exception as e: - logger.error(f"处理消息时发生错误: {e}") - return - - if not processed_message: - logger.critical("现在暂时不支持解析此回复!") - return None - - if group_info and user_info: - logger.debug("发送群聊消息") - target_id = int(group_info.group_id) if group_info.group_id else None - action = "send_group_msg" - id_name = "group_id" - elif user_info: - logger.debug("发送私聊消息") - target_id = int(user_info.user_id) if user_info.user_id else None - action = "send_private_msg" - id_name = "user_id" - else: - logger.error("无法识别的消息类型") - return - logger.info("尝试发送到napcat") - logger.debug( - f"准备发送到napcat的消息体: action='{action}', {id_name}='{target_id}', message='{processed_message}'" - ) - response = await self.send_message_to_napcat( - action, - { - id_name: target_id, - "message": processed_message, - }, - ) - if response.get("status") == "ok": - logger.info("消息发送成功") - qq_message_id = response.get("data", {}).get("message_id") - await self.message_sent_back(raw_message_base, qq_message_id) - else: - logger.warning(f"消息发送失败,napcat返回:{str(response)}") - - async def send_command(self, raw_message_base: MessageBase) -> None: - """ - 处理命令类 - """ - logger.info("处理命令中") - message_info: BaseMessageInfo = raw_message_base.message_info - message_segment: Seg = raw_message_base.message_segment - group_info: Optional[GroupInfo] = message_info.group_info - seg_data: Dict[str, Any] = message_segment.data if isinstance(message_segment.data, dict) else {} - command_name: Optional[str] = seg_data.get("name") - try: - args = seg_data.get("args", {}) - if not isinstance(args, dict): - args = {} - - match command_name: - case CommandType.GROUP_BAN.name: - command, args_dict = self.handle_ban_command(args, group_info) - case CommandType.GROUP_WHOLE_BAN.name: - command, args_dict = self.handle_whole_ban_command(args, group_info) - case CommandType.GROUP_KICK.name: - command, args_dict = self.handle_kick_command(args, group_info) - case CommandType.SEND_POKE.name: - command, args_dict = self.handle_poke_command(args, group_info) - case CommandType.DELETE_MSG.name: - command, args_dict = self.delete_msg_command(args) - case CommandType.AI_VOICE_SEND.name: - command, args_dict = self.handle_ai_voice_send_command(args, group_info) - case CommandType.SET_EMOJI_LIKE.name: - command, args_dict = self.handle_set_emoji_like_command(args) - case CommandType.SEND_AT_MESSAGE.name: - command, args_dict = self.handle_at_message_command(args, group_info) - case CommandType.SEND_LIKE.name: - command, args_dict = self.handle_send_like_command(args) - case _: - logger.error(f"未知命令: {command_name}") - return - except Exception as e: - logger.error(f"处理命令时发生错误: {e}") - return None - - if not command or not args_dict: - logger.error("命令或参数缺失") - return None - - logger.info(f"准备向 Napcat 发送命令: command='{command}', args_dict='{args_dict}'") - response = await self.send_message_to_napcat(command, args_dict) - logger.info(f"收到 Napcat 的命令响应: {response}") - - if response.get("status") == "ok": - logger.info(f"命令 {command_name} 执行成功") - else: - logger.warning(f"命令 {command_name} 执行失败,napcat返回:{str(response)}") - - async def handle_adapter_command(self, raw_message_base: MessageBase) -> None: - """ - 处理适配器命令类 - 用于直接向Napcat发送命令并返回结果 - """ - logger.info("处理适配器命令中") - message_segment: Seg = raw_message_base.message_segment - seg_data: Dict[str, Any] = message_segment.data if isinstance(message_segment.data, dict) else {} - - try: - action = seg_data.get("action") - params = seg_data.get("params", {}) - request_id = seg_data.get("request_id") - - if not action: - logger.error("适配器命令缺少action参数") - await self.send_adapter_command_response( - raw_message_base, {"status": "error", "message": "缺少action参数"}, request_id - ) - return - - logger.info(f"执行适配器命令: {action}") - - # 根据action决定处理方式 - if action == "get_cookies": - # 对于get_cookies,我们需要一个更长的超时时间 - response = await self.send_message_to_napcat(action, params, timeout=40.0) - else: - # 对于其他命令,使用默认超时 - response = await self.send_message_to_napcat(action, params) - - # 发送响应回MoFox-Bot - logger.debug(f"[DEBUG handle_adapter_command] 即将调用send_adapter_command_response, request_id={request_id}") - await self.send_adapter_command_response(raw_message_base, response, request_id) - logger.debug("[DEBUG handle_adapter_command] send_adapter_command_response调用完成") - - if response.get("status") == "ok": - logger.info(f"适配器命令 {action} 执行成功") - else: - logger.warning(f"适配器命令 {action} 执行失败,napcat返回:{str(response)}") - # 无论成功失败,都记录下完整的响应内容以供调试 - logger.debug(f"适配器命令 {action} 的完整响应: {response}") - - except Exception as e: - logger.error(f"处理适配器命令时发生错误: {e}") - error_response = {"status": "error", "message": str(e)} - await self.send_adapter_command_response(raw_message_base, error_response, seg_data.get("request_id")) - - def get_level(self, seg_data: Seg) -> int: - if seg_data.type == "seglist": - return 1 + max(self.get_level(seg) for seg in seg_data.data) - else: - return 1 - - async def handle_seg_recursive(self, seg_data: Seg, user_info: UserInfo) -> list: - payload: list = [] - if seg_data.type == "seglist": - # level = self.get_level(seg_data) # 给以后可能的多层嵌套做准备,此处不使用 - if not seg_data.data: - return [] - for seg in seg_data.data: - payload = await self.process_message_by_type(seg, payload, user_info) - else: - payload = await self.process_message_by_type(seg_data, payload, user_info) - return payload - - async def process_message_by_type(self, seg: Seg, payload: list, user_info: UserInfo) -> list: - # sourcery skip: reintroduce-else, swap-if-else-branches, use-named-expression - new_payload = payload - if seg.type == "reply": - target_id = seg.data - target_id = str(target_id) - if target_id == "notice": - return payload - logger.info(target_id if isinstance(target_id, str) else "") - new_payload = self.build_payload( - payload, - await self.handle_reply_message(target_id if isinstance(target_id, str) else "", user_info), - True, - ) - elif seg.type == "text": - text = seg.data - if not text: - return payload - new_payload = self.build_payload( - payload, - self.handle_text_message(text if isinstance(text, str) else ""), - False, - ) - elif seg.type == "face": - logger.warning("MoFox-Bot 发送了qq原生表情,暂时不支持") - elif seg.type == "image": - image = seg.data - new_payload = self.build_payload(payload, self.handle_image_message(image), False) - elif seg.type == "emoji": - emoji = seg.data - new_payload = self.build_payload(payload, self.handle_emoji_message(emoji), False) - elif seg.type == "voice": - voice = seg.data - new_payload = self.build_payload(payload, self.handle_voice_message(voice), False) - elif seg.type == "voiceurl": - voice_url = seg.data - new_payload = self.build_payload(payload, self.handle_voiceurl_message(voice_url), False) - elif seg.type == "music": - song_id = seg.data - new_payload = self.build_payload(payload, self.handle_music_message(song_id), False) - elif seg.type == "videourl": - video_url = seg.data - new_payload = self.build_payload(payload, self.handle_videourl_message(video_url), False) - elif seg.type == "file": - file_path = seg.data - new_payload = self.build_payload(payload, self.handle_file_message(file_path), False) - return new_payload - - def build_payload(self, payload: list, addon: dict | list, is_reply: bool = False) -> list: - # sourcery skip: for-append-to-extend, merge-list-append, simplify-generator - """构建发送的消息体""" - if is_reply: - temp_list = [] - if isinstance(addon, list): - temp_list.extend(addon) - else: - temp_list.append(addon) - for i in payload: - if i.get("type") == "reply": - logger.debug("检测到多个回复,使用最新的回复") - continue - temp_list.append(i) - return temp_list - else: - if isinstance(addon, list): - payload.extend(addon) - else: - payload.append(addon) - return payload - - async def handle_reply_message(self, id: str, user_info: UserInfo) -> dict | list: - """处理回复消息""" - logger.debug(f"开始处理回复消息,消息ID: {id}") - reply_seg = {"type": "reply", "data": {"id": id}} - - # 检查是否启用引用艾特功能 - if not config_api.get_plugin_config(self.plugin_config, "features.enable_reply_at", False): - logger.info("引用艾特功能未启用,仅发送普通回复") - return reply_seg - - try: - msg_info_response = await self.send_message_to_napcat("get_msg", {"message_id": id}) - logger.debug(f"获取消息 {id} 的详情响应: {msg_info_response}") - - replied_user_id = None - if msg_info_response and msg_info_response.get("status") == "ok": - sender_info = msg_info_response.get("data", {}).get("sender") - if sender_info: - replied_user_id = sender_info.get("user_id") - - # 如果没有获取到被回复者的ID,则直接返回,不进行@ - if not replied_user_id: - logger.warning(f"无法获取消息 {id} 的发送者信息,跳过 @") - logger.info(f"最终返回的回复段: {reply_seg}") - return reply_seg - - # 根据概率决定是否艾特用户 - if random.random() < config_api.get_plugin_config(self.plugin_config, "features.reply_at_rate", 0.5): - at_seg = {"type": "at", "data": {"qq": str(replied_user_id)}} - # 在艾特后面添加一个空格 - text_seg = {"type": "text", "data": {"text": " "}} - result_seg = [reply_seg, at_seg, text_seg] - logger.info(f"最终返回的回复段: {result_seg}") - return result_seg - - except Exception as e: - logger.error(f"处理引用回复并尝试@时出错: {e}") - # 出现异常时,只发送普通的回复,避免程序崩溃 - logger.info(f"最终返回的回复段: {reply_seg}") - return reply_seg - - logger.info(f"最终返回的回复段: {reply_seg}") - return reply_seg - - def handle_text_message(self, message: str) -> dict: - """处理文本消息""" - return {"type": "text", "data": {"text": message}} - - def handle_image_message(self, encoded_image: str) -> dict: - """处理图片消息""" - return { - "type": "image", - "data": { - "file": f"base64://{encoded_image}", - "subtype": 0, - }, - } # base64 编码的图片 - - def handle_emoji_message(self, encoded_emoji: str) -> dict: - """处理表情消息""" - encoded_image = encoded_emoji - image_format = get_image_format(encoded_emoji) - if image_format != "gif": - encoded_image = convert_image_to_gif(encoded_emoji) - return { - "type": "image", - "data": { - "file": f"base64://{encoded_image}", - "subtype": 1, - "summary": "[动画表情]", - }, - } - - def handle_voice_message(self, encoded_voice: str) -> dict: - """处理语音消息""" - use_tts = False - if self.plugin_config: - use_tts = config_api.get_plugin_config(self.plugin_config, "voice.use_tts", False) - - if not use_tts: - logger.warning("未启用语音消息处理") - return {} - if not encoded_voice: - return {} - return { - "type": "record", - "data": {"file": f"base64://{encoded_voice}"}, - } - - def handle_voiceurl_message(self, voice_url: str) -> dict: - """处理语音链接消息""" - return { - "type": "record", - "data": {"file": voice_url}, - } - - def handle_music_message(self, song_id: str) -> dict: - """处理音乐消息""" - return { - "type": "music", - "data": {"type": "163", "id": song_id}, - } - - def handle_videourl_message(self, video_url: str) -> dict: - """处理视频链接消息""" - return { - "type": "video", - "data": {"file": video_url}, - } - - def handle_file_message(self, file_path: str) -> dict: - """处理文件消息""" - return { - "type": "file", - "data": {"file": f"file://{file_path}"}, - } - - def delete_msg_command(self, args: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]: - """处理删除消息命令""" - return "delete_msg", {"message_id": args["message_id"]} - - def handle_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """处理封禁命令 - - Args: - args (Dict[str, Any]): 参数字典 - group_info (GroupInfo): 群聊信息(对应目标群聊) - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - duration: int = int(args["duration"]) - user_id: int = int(args["qq_id"]) - group_id: int = int(group_info.group_id) - if duration < 0: - raise ValueError("封禁时间必须大于等于0") - if not user_id or not group_id: - raise ValueError("封禁命令缺少必要参数") - if duration > 2592000: - raise ValueError("封禁时间不能超过30天") - return ( - CommandType.GROUP_BAN.value, - { - "group_id": group_id, - "user_id": user_id, - "duration": duration, - }, - ) - - def handle_whole_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """处理全体禁言命令 - - Args: - args (Dict[str, Any]): 参数字典 - group_info (GroupInfo): 群聊信息(对应目标群聊) - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - enable = args["enable"] - assert isinstance(enable, bool), "enable参数必须是布尔值" - group_id: int = int(group_info.group_id) - if group_id <= 0: - raise ValueError("群组ID无效") - return ( - CommandType.GROUP_WHOLE_BAN.value, - { - "group_id": group_id, - "enable": enable, - }, - ) - - def handle_kick_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """处理群成员踢出命令 - - Args: - args (Dict[str, Any]): 参数字典 - group_info (GroupInfo): 群聊信息(对应目标群聊) - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - user_id: int = int(args["qq_id"]) - group_id: int = int(group_info.group_id) - if group_id <= 0: - raise ValueError("群组ID无效") - if user_id <= 0: - raise ValueError("用户ID无效") - return ( - CommandType.GROUP_KICK.value, - { - "group_id": group_id, - "user_id": user_id, - "reject_add_request": False, # 不拒绝加群请求 - }, - ) - - def handle_poke_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """处理戳一戳命令 - - Args: - args (Dict[str, Any]): 参数字典 - group_info (GroupInfo): 群聊信息(对应目标群聊) - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - user_id: int = int(args["qq_id"]) - if group_info is None: - group_id = None - else: - group_id: int = int(group_info.group_id) - if group_id <= 0: - raise ValueError("群组ID无效") - if user_id <= 0: - raise ValueError("用户ID无效") - return ( - CommandType.SEND_POKE.value, - { - "group_id": group_id, - "user_id": user_id, - }, - ) - - def handle_set_emoji_like_command(self, args: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]: - """处理设置表情回应命令 - - Args: - args (Dict[str, Any]): 参数字典 - - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - logger.info(f"开始处理表情回应命令, 接收到参数: {args}") - try: - message_id = int(args["message_id"]) - emoji_id = int(args["emoji_id"]) - set_like = bool(args["set"]) - except (KeyError, ValueError) as e: - logger.error(f"处理表情回应命令时发生错误: {e}, 原始参数: {args}") - raise ValueError(f"缺少必需参数或参数类型错误: {e}") - - return ( - CommandType.SET_EMOJI_LIKE.value, - {"message_id": message_id, "emoji_id": emoji_id, "set": set_like}, - ) - - def handle_send_like_command(self, args: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]: - """ - 处理发送点赞命令的逻辑。 - - Args: - args (Dict[str, Any]): 参数字典 - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - try: - user_id: int = int(args["qq_id"]) - times: int = int(args["times"]) - except (KeyError, ValueError): - raise ValueError("缺少必需参数: qq_id 或 times") - - return ( - CommandType.SEND_LIKE.value, - {"user_id": user_id, "times": times}, - ) - - def handle_ai_voice_send_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """ - 处理AI语音发送命令的逻辑。 - 并返回 NapCat 兼容的 (action, params) 元组。 - """ - if not group_info or not group_info.group_id: - raise ValueError("AI语音发送命令必须在群聊上下文中使用") - if not args: - raise ValueError("AI语音发送命令缺少参数") - - group_id: int = int(group_info.group_id) - character_id = args.get("character") - text_content = args.get("text") - - if not character_id or not text_content: - raise ValueError(f"AI语音发送命令参数不完整: character='{character_id}', text='{text_content}'") - - return ( - CommandType.AI_VOICE_SEND.value, - { - "group_id": group_id, - "text": text_content, - "character": character_id, - }, - ) - - async def send_message_to_napcat(self, action: str, params: dict, timeout: float = 20.0) -> dict: - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps({"action": action, "params": params, "echo": request_uuid}).decode('utf-8') - - # 获取当前连接 - connection = self.get_server_connection() - if not connection: - logger.error("没有可用的 Napcat 连接") - return {"status": "error", "message": "no connection"} - - try: - await connection.send(payload) - response = await get_response(request_uuid, timeout=timeout) # 使用传入的超时时间 - except TimeoutError: - logger.error(f"发送消息超时({timeout}秒),未收到响应: action={action}, params={params}") - return {"status": "error", "message": "timeout"} - except Exception as e: - logger.error(f"发送消息失败: {e}") - return {"status": "error", "message": str(e)} - return response - - async def message_sent_back(self, message_base: MessageBase, qq_message_id: str) -> None: - # 修改 additional_config,添加 echo 字段 - if message_base.message_info.additional_config is None: - message_base.message_info.additional_config = {} - - message_base.message_info.additional_config["echo"] = True - - # 获取原始的 mmc_message_id - mmc_message_id = message_base.message_info.message_id - - # 修改 message_segment 为 notify 类型 - message_base.message_segment = Seg( - type="notify", data={"sub_type": "echo", "echo": mmc_message_id, "actual_id": qq_message_id} - ) - await message_send_instance.message_send(message_base) - logger.debug("已回送消息ID") - return - - async def send_adapter_command_response( - self, original_message: MessageBase, response_data: dict, request_id: str - ) -> None: - """ - 发送适配器命令响应回MoFox-Bot - - Args: - original_message: 原始消息 - response_data: 响应数据 - request_id: 请求ID - """ - try: - # 修改 additional_config,添加 echo 字段 - if original_message.message_info.additional_config is None: - original_message.message_info.additional_config = {} - - original_message.message_info.additional_config["echo"] = True - - # 修改 message_segment 为 adapter_response 类型 - original_message.message_segment = Seg( - type="adapter_response", - data={"request_id": request_id, "response": response_data, "timestamp": int(time.time() * 1000)}, - ) - - await message_send_instance.message_send(original_message) - - except Exception as e: - logger.error(f"发送适配器命令响应时出错: {e}") - - def handle_at_message_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """处理艾特并发送消息命令 - - Args: - args (Dict[str, Any]): 参数字典, 包含 qq_id 和 text - group_info (GroupInfo): 群聊信息 - - Returns: - Tuple[str, Dict[str, Any]]: (action, params) - """ - at_user_id = args.get("qq_id") - text = args.get("text") - - if not at_user_id or not text: - raise ValueError("艾特消息命令缺少 qq_id 或 text 参数") - - if not group_info: - raise ValueError("艾特消息命令必须在群聊上下文中使用") - - message_payload = [ - {"type": "at", "data": {"qq": str(at_user_id)}}, - {"type": "text", "data": {"text": " " + str(text)}}, - ] - - return ( - "send_group_msg", - { - "group_id": group_info.group_id, - "message": message_payload, - }, - ) - - -send_handler = SendHandler() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/stream_router.py b/src/plugins/built_in/napcat_adapter_plugin/src/stream_router.py deleted file mode 100644 index f8d9fb49f..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/stream_router.py +++ /dev/null @@ -1,351 +0,0 @@ -""" -按聊天流分配消费者的消息路由系统 - -核心思想: -- 为每个活跃的聊天流(stream_id)创建独立的消息队列和消费者协程 -- 同一聊天流的消息由同一个 worker 处理,保证顺序性 -- 不同聊天流的消息并发处理,提高吞吐量 -- 动态管理流的生命周期,自动清理不活跃的流 -""" - -import asyncio -import time -from typing import Dict, Optional - -from src.common.logger import get_logger - -logger = get_logger("stream_router") - - -class StreamConsumer: - """单个聊天流的消息消费者 - - 维护独立的消息队列和处理协程 - """ - - def __init__(self, stream_id: str, queue_maxsize: int = 100): - self.stream_id = stream_id - self.queue: asyncio.Queue = asyncio.Queue(maxsize=queue_maxsize) - self.worker_task: Optional[asyncio.Task] = None - self.last_active_time = time.time() - self.is_running = False - - # 性能统计 - self.stats = { - "total_messages": 0, - "total_processing_time": 0.0, - "queue_overflow_count": 0, - } - - async def start(self) -> None: - """启动消费者""" - if not self.is_running: - self.is_running = True - self.worker_task = asyncio.create_task(self._process_loop()) - logger.debug(f"Stream Consumer 启动: {self.stream_id}") - - async def stop(self) -> None: - """停止消费者""" - self.is_running = False - if self.worker_task: - self.worker_task.cancel() - try: - await self.worker_task - except asyncio.CancelledError: - pass - logger.debug(f"Stream Consumer 停止: {self.stream_id}") - - async def enqueue(self, message: dict) -> None: - """将消息加入队列""" - self.last_active_time = time.time() - - try: - # 使用 put_nowait 避免阻塞路由器 - self.queue.put_nowait(message) - except asyncio.QueueFull: - self.stats["queue_overflow_count"] += 1 - logger.warning( - f"Stream {self.stream_id} 队列已满 " - f"({self.queue.qsize()}/{self.queue.maxsize})," - f"消息被丢弃!溢出次数: {self.stats['queue_overflow_count']}" - ) - # 可选策略:丢弃最旧的消息 - # try: - # self.queue.get_nowait() - # self.queue.put_nowait(message) - # logger.debug(f"Stream {self.stream_id} 丢弃最旧消息,添加新消息") - # except asyncio.QueueEmpty: - # pass - - async def _process_loop(self) -> None: - """消息处理循环""" - # 延迟导入,避免循环依赖 - from .recv_handler.message_handler import message_handler - from .recv_handler.meta_event_handler import meta_event_handler - from .recv_handler.notice_handler import notice_handler - - logger.info(f"Stream {self.stream_id} 处理循环启动") - - try: - while self.is_running: - try: - # 等待消息,1秒超时 - message = await asyncio.wait_for( - self.queue.get(), - timeout=1.0 - ) - - start_time = time.time() - - # 处理消息 - post_type = message.get("post_type") - if post_type == "message": - await message_handler.handle_raw_message(message) - elif post_type == "meta_event": - await meta_event_handler.handle_meta_event(message) - elif post_type == "notice": - await notice_handler.handle_notice(message) - else: - logger.warning(f"未知的 post_type: {post_type}") - - processing_time = time.time() - start_time - - # 更新统计 - self.stats["total_messages"] += 1 - self.stats["total_processing_time"] += processing_time - self.last_active_time = time.time() - self.queue.task_done() - - # 性能监控(每100条消息输出一次) - if self.stats["total_messages"] % 100 == 0: - avg_time = self.stats["total_processing_time"] / self.stats["total_messages"] - logger.info( - f"Stream {self.stream_id[:30]}... 统计: " - f"消息数={self.stats['total_messages']}, " - f"平均耗时={avg_time:.3f}秒, " - f"队列长度={self.queue.qsize()}" - ) - - # 动态延迟:队列空时短暂休眠 - if self.queue.qsize() == 0: - await asyncio.sleep(0.01) - - except asyncio.TimeoutError: - # 超时是正常的,继续循环 - continue - except asyncio.CancelledError: - logger.info(f"Stream {self.stream_id} 处理循环被取消") - break - except Exception as e: - logger.error(f"Stream {self.stream_id} 处理消息时出错: {e}", exc_info=True) - # 继续处理下一条消息 - await asyncio.sleep(0.1) - - finally: - logger.info(f"Stream {self.stream_id} 处理循环结束") - - def get_stats(self) -> dict: - """获取性能统计""" - avg_time = ( - self.stats["total_processing_time"] / self.stats["total_messages"] - if self.stats["total_messages"] > 0 - else 0 - ) - - return { - "stream_id": self.stream_id, - "queue_size": self.queue.qsize(), - "total_messages": self.stats["total_messages"], - "avg_processing_time": avg_time, - "queue_overflow_count": self.stats["queue_overflow_count"], - "last_active_time": self.last_active_time, - } - - -class StreamRouter: - """流路由器 - - 负责将消息路由到对应的聊天流队列 - 动态管理聊天流的生命周期 - """ - - def __init__( - self, - max_streams: int = 500, - stream_timeout: int = 600, - stream_queue_size: int = 100, - cleanup_interval: int = 60, - ): - self.streams: Dict[str, StreamConsumer] = {} - self.lock = asyncio.Lock() - self.max_streams = max_streams - self.stream_timeout = stream_timeout - self.stream_queue_size = stream_queue_size - self.cleanup_interval = cleanup_interval - self.cleanup_task: Optional[asyncio.Task] = None - self.is_running = False - - async def start(self) -> None: - """启动路由器""" - if not self.is_running: - self.is_running = True - self.cleanup_task = asyncio.create_task(self._cleanup_loop()) - logger.info( - f"StreamRouter 已启动 - " - f"最大流数: {self.max_streams}, " - f"超时: {self.stream_timeout}秒, " - f"队列大小: {self.stream_queue_size}" - ) - - async def stop(self) -> None: - """停止路由器""" - self.is_running = False - - if self.cleanup_task: - self.cleanup_task.cancel() - try: - await self.cleanup_task - except asyncio.CancelledError: - pass - - # 停止所有流消费者 - logger.info(f"正在停止 {len(self.streams)} 个流消费者...") - for consumer in self.streams.values(): - await consumer.stop() - - self.streams.clear() - logger.info("StreamRouter 已停止") - - async def route_message(self, message: dict) -> None: - """路由消息到对应的流""" - stream_id = self._extract_stream_id(message) - - # 快速路径:流已存在 - if stream_id in self.streams: - await self.streams[stream_id].enqueue(message) - return - - # 慢路径:需要创建新流 - async with self.lock: - # 双重检查 - if stream_id not in self.streams: - # 检查流数量限制 - if len(self.streams) >= self.max_streams: - logger.warning( - f"达到最大流数量限制 ({self.max_streams})," - f"尝试清理不活跃的流..." - ) - await self._cleanup_inactive_streams() - - # 清理后仍然超限,记录警告但继续创建 - if len(self.streams) >= self.max_streams: - logger.error( - f"清理后仍达到最大流数量 ({len(self.streams)}/{self.max_streams})!" - ) - - # 创建新流 - consumer = StreamConsumer(stream_id, self.stream_queue_size) - self.streams[stream_id] = consumer - await consumer.start() - logger.info(f"创建新的 Stream Consumer: {stream_id} (总流数: {len(self.streams)})") - - await self.streams[stream_id].enqueue(message) - - def _extract_stream_id(self, message: dict) -> str: - """从消息中提取 stream_id - - 返回格式: platform:id:type - 例如: qq:123456:group 或 qq:789012:private - """ - post_type = message.get("post_type") - - # 非消息类型,使用默认流(避免创建过多流) - if post_type not in ["message", "notice"]: - return "system:meta_event" - - # 消息类型 - if post_type == "message": - message_type = message.get("message_type") - if message_type == "group": - group_id = message.get("group_id") - return f"qq:{group_id}:group" - elif message_type == "private": - user_id = message.get("user_id") - return f"qq:{user_id}:private" - - # notice 类型 - elif post_type == "notice": - group_id = message.get("group_id") - if group_id: - return f"qq:{group_id}:group" - user_id = message.get("user_id") - if user_id: - return f"qq:{user_id}:private" - - # 未知类型,使用通用流 - return "unknown:unknown" - - async def _cleanup_inactive_streams(self) -> None: - """清理不活跃的流""" - current_time = time.time() - to_remove = [] - - for stream_id, consumer in self.streams.items(): - if current_time - consumer.last_active_time > self.stream_timeout: - to_remove.append(stream_id) - - for stream_id in to_remove: - await self.streams[stream_id].stop() - del self.streams[stream_id] - logger.debug(f"清理不活跃的流: {stream_id}") - - if to_remove: - logger.info( - f"清理了 {len(to_remove)} 个不活跃的流 " - f"(当前活跃流: {len(self.streams)}/{self.max_streams})" - ) - - async def _cleanup_loop(self) -> None: - """定期清理循环""" - logger.info(f"清理循环已启动,间隔: {self.cleanup_interval}秒") - try: - while self.is_running: - await asyncio.sleep(self.cleanup_interval) - await self._cleanup_inactive_streams() - except asyncio.CancelledError: - logger.info("清理循环已停止") - - def get_all_stats(self) -> list[dict]: - """获取所有流的统计信息""" - return [consumer.get_stats() for consumer in self.streams.values()] - - def get_summary(self) -> dict: - """获取路由器摘要""" - total_messages = sum(c.stats["total_messages"] for c in self.streams.values()) - total_queue_size = sum(c.queue.qsize() for c in self.streams.values()) - total_overflows = sum(c.stats["queue_overflow_count"] for c in self.streams.values()) - - # 计算平均队列长度 - avg_queue_size = total_queue_size / len(self.streams) if self.streams else 0 - - # 找出最繁忙的流 - busiest_stream = None - if self.streams: - busiest_stream = max( - self.streams.values(), - key=lambda c: c.stats["total_messages"] - ).stream_id - - return { - "total_streams": len(self.streams), - "max_streams": self.max_streams, - "total_messages_processed": total_messages, - "total_queue_size": total_queue_size, - "avg_queue_size": avg_queue_size, - "total_queue_overflows": total_overflows, - "busiest_stream": busiest_stream, - } - - -# 全局路由器实例 -stream_router = StreamRouter() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/utils.py b/src/plugins/built_in/napcat_adapter_plugin/src/utils.py deleted file mode 100644 index b597a60f9..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/utils.py +++ /dev/null @@ -1,314 +0,0 @@ -import base64 -import io -import orjson -import ssl -import uuid -from typing import List, Optional, Tuple, Union - -import urllib3 -import websockets as Server -from PIL import Image - -from src.common.logger import get_logger - -from .database import BanUser, napcat_db -from .response_pool import get_response - -logger = get_logger("napcat_adapter") - - -class SSLAdapter(urllib3.PoolManager): - def __init__(self, *args, **kwargs): - context = ssl.create_default_context() - context.set_ciphers("DEFAULT@SECLEVEL=1") - context.minimum_version = ssl.TLSVersion.TLSv1_2 - kwargs["ssl_context"] = context - super().__init__(*args, **kwargs) - - -async def get_group_info(websocket: Server.ServerConnection, group_id: int) -> dict | None: - """ - 获取群相关信息 - - 返回值需要处理可能为空的情况 - """ - logger.debug("获取群聊信息中") - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps({"action": "get_group_info", "params": {"group_id": group_id}, "echo": request_uuid}).decode('utf-8') - try: - await websocket.send(payload) - socket_response: dict = await get_response(request_uuid) - except TimeoutError: - logger.error(f"获取群信息超时,群号: {group_id}") - return None - except Exception as e: - logger.error(f"获取群信息失败: {e}") - return None - logger.debug(socket_response) - return socket_response.get("data") - - -async def get_group_detail_info(websocket: Server.ServerConnection, group_id: int) -> dict | None: - """ - 获取群详细信息 - - 返回值需要处理可能为空的情况 - """ - logger.debug("获取群详细信息中") - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps({"action": "get_group_detail_info", "params": {"group_id": group_id}, "echo": request_uuid}).decode('utf-8') - try: - await websocket.send(payload) - socket_response: dict = await get_response(request_uuid) - except TimeoutError: - logger.error(f"获取群详细信息超时,群号: {group_id}") - return None - except Exception as e: - logger.error(f"获取群详细信息失败: {e}") - return None - logger.debug(socket_response) - return socket_response.get("data") - - -async def get_member_info(websocket: Server.ServerConnection, group_id: int, user_id: int) -> dict | None: - """ - 获取群成员信息 - - 返回值需要处理可能为空的情况 - """ - logger.debug("获取群成员信息中") - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps( - { - "action": "get_group_member_info", - "params": {"group_id": group_id, "user_id": user_id, "no_cache": True}, - "echo": request_uuid, - } - ).decode('utf-8') - try: - await websocket.send(payload) - socket_response: dict = await get_response(request_uuid) - except TimeoutError: - logger.error(f"获取成员信息超时,群号: {group_id}, 用户ID: {user_id}") - return None - except Exception as e: - logger.error(f"获取成员信息失败: {e}") - return None - logger.debug(socket_response) - return socket_response.get("data") - - -async def get_image_base64(url: str) -> str: - # sourcery skip: raise-specific-error - """获取图片/表情包的Base64""" - logger.debug(f"下载图片: {url}") - http = SSLAdapter() - try: - response = http.request("GET", url, timeout=10) - if response.status != 200: - raise Exception(f"HTTP Error: {response.status}") - image_bytes = response.data - return base64.b64encode(image_bytes).decode("utf-8") - except Exception as e: - logger.error(f"图片下载失败: {str(e)}") - raise - - -def convert_image_to_gif(image_base64: str) -> str: - # sourcery skip: extract-method - """ - 将Base64编码的图片转换为GIF格式 - Parameters: - image_base64: str: Base64编码的图片数据 - Returns: - str: Base64编码的GIF图片数据 - """ - logger.debug("转换图片为GIF格式") - try: - image_bytes = base64.b64decode(image_base64) - image = Image.open(io.BytesIO(image_bytes)) - output_buffer = io.BytesIO() - image.save(output_buffer, format="GIF") - output_buffer.seek(0) - return base64.b64encode(output_buffer.read()).decode("utf-8") - except Exception as e: - logger.error(f"图片转换为GIF失败: {str(e)}") - return image_base64 - - -async def get_self_info(websocket: Server.ServerConnection) -> dict | None: - """ - 获取自身信息 - Parameters: - websocket: WebSocket连接对象 - Returns: - data: dict: 返回的自身信息 - """ - logger.debug("获取自身信息中") - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps({"action": "get_login_info", "params": {}, "echo": request_uuid}).decode('utf-8') - try: - await websocket.send(payload) - response: dict = await get_response(request_uuid) - except TimeoutError: - logger.error("获取自身信息超时") - return None - except Exception as e: - logger.error(f"获取自身信息失败: {e}") - return None - logger.debug(response) - return response.get("data") - - -def get_image_format(raw_data: str) -> str: - """ - 从Base64编码的数据中确定图片的格式。 - Parameters: - raw_data: str: Base64编码的图片数据。 - Returns: - format: str: 图片的格式(例如 'jpeg', 'png', 'gif')。 - """ - image_bytes = base64.b64decode(raw_data) - return Image.open(io.BytesIO(image_bytes)).format.lower() - - -async def get_stranger_info(websocket: Server.ServerConnection, user_id: int) -> dict | None: - """ - 获取陌生人信息 - Parameters: - websocket: WebSocket连接对象 - user_id: 用户ID - Returns: - dict: 返回的陌生人信息 - """ - logger.debug("获取陌生人信息中") - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps({"action": "get_stranger_info", "params": {"user_id": user_id}, "echo": request_uuid}).decode('utf-8') - try: - await websocket.send(payload) - response: dict = await get_response(request_uuid) - except TimeoutError: - logger.error(f"获取陌生人信息超时,用户ID: {user_id}") - return None - except Exception as e: - logger.error(f"获取陌生人信息失败: {e}") - return None - logger.debug(response) - return response.get("data") - - -async def get_message_detail(websocket: Server.ServerConnection, message_id: Union[str, int]) -> dict | None: - """ - 获取消息详情,可能为空 - Parameters: - websocket: WebSocket连接对象 - message_id: 消息ID - Returns: - dict: 返回的消息详情 - """ - logger.debug("获取消息详情中") - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps({"action": "get_msg", "params": {"message_id": message_id}, "echo": request_uuid}).decode('utf-8') - try: - await websocket.send(payload) - response: dict = await get_response(request_uuid, 30) # 增加超时时间到30秒 - except TimeoutError: - logger.error(f"获取消息详情超时,消息ID: {message_id}") - return None - except Exception as e: - logger.error(f"获取消息详情失败: {e}") - return None - logger.debug(response) - return response.get("data") - - -async def get_record_detail( - websocket: Server.ServerConnection, file: str, file_id: Optional[str] = None -) -> dict | None: - """ - 获取语音消息内容 - Parameters: - websocket: WebSocket连接对象 - file: 文件名 - file_id: 文件ID - Returns: - dict: 返回的语音消息详情 - """ - logger.debug("获取语音消息详情中") - request_uuid = str(uuid.uuid4()) - payload = orjson.dumps( - { - "action": "get_record", - "params": {"file": file, "file_id": file_id, "out_format": "wav"}, - "echo": request_uuid, - } - ).decode('utf-8') - try: - await websocket.send(payload) - response: dict = await get_response(request_uuid, 30) # 增加超时时间到30秒 - except TimeoutError: - logger.error(f"获取语音消息详情超时,文件: {file}, 文件ID: {file_id}") - return None - except Exception as e: - logger.error(f"获取语音消息详情失败: {e}") - return None - logger.debug(f"{str(response)[:200]}...") # 防止语音的超长base64编码导致日志过长 - return response.get("data") - - -async def read_ban_list( - websocket: Server.ServerConnection, -) -> Tuple[List[BanUser], List[BanUser]]: - """ - 从根目录下的data文件夹中的文件读取禁言列表。 - 同时自动更新已经失效禁言 - Returns: - Tuple[ - 一个仍在禁言中的用户的BanUser列表, - 一个已经自然解除禁言的用户的BanUser列表, - 一个仍在全体禁言中的群的BanUser列表, - 一个已经自然解除全体禁言的群的BanUser列表, - ] - """ - try: - ban_list = await napcat_db.get_ban_records() - lifted_list: List[BanUser] = [] - logger.info("已经读取禁言列表") - # 复制列表以避免迭代中修改原列表问题 - for ban_record in list(ban_list): - if ban_record.user_id == 0: - fetched_group_info = await get_group_info(websocket, ban_record.group_id) - if fetched_group_info is None: - logger.warning(f"无法获取群信息,群号: {ban_record.group_id},默认禁言解除") - lifted_list.append(ban_record) - ban_list.remove(ban_record) - continue - group_all_shut: int = fetched_group_info.get("group_all_shut") - if group_all_shut == 0: - lifted_list.append(ban_record) - ban_list.remove(ban_record) - continue - else: - fetched_member_info = await get_member_info(websocket, ban_record.group_id, ban_record.user_id) - if fetched_member_info is None: - logger.warning( - f"无法获取群成员信息,用户ID: {ban_record.user_id}, 群号: {ban_record.group_id},默认禁言解除" - ) - lifted_list.append(ban_record) - ban_list.remove(ban_record) - continue - lift_ban_time: int = fetched_member_info.get("shut_up_timestamp") - if lift_ban_time == 0: - lifted_list.append(ban_record) - ban_list.remove(ban_record) - else: - ban_record.lift_time = lift_ban_time - await napcat_db.update_ban_record(ban_list) - return ban_list, lifted_list - except Exception as e: - logger.error(f"读取禁言列表失败: {e}") - return [], [] - - -async def save_ban_record(list: List[BanUser]): - return await napcat_db.update_ban_record(list) \ No newline at end of file diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/video_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/video_handler.py deleted file mode 100644 index aa64d2571..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/video_handler.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -视频下载和处理模块 -用于从QQ消息中下载视频并转发给Bot进行分析 -""" - -import asyncio -from pathlib import Path -from typing import Any, Dict, Optional - -import aiohttp - -from src.common.logger import get_logger - -logger = get_logger("video_handler") - - -class VideoDownloader: - def __init__(self, max_size_mb: int = 100, download_timeout: int = 60): - self.max_size_mb = max_size_mb - self.download_timeout = download_timeout - self.supported_formats = {".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".webm", ".m4v"} - - def is_video_url(self, url: str) -> bool: - """检查URL是否为视频文件""" - try: - # QQ视频URL可能没有扩展名,所以先检查Content-Type - # 对于QQ视频,我们先假设是视频,稍后通过Content-Type验证 - - # 检查URL中是否包含视频相关的关键字 - video_keywords = ["video", "mp4", "avi", "mov", "mkv", "flv", "wmv", "webm", "m4v"] - url_lower = url.lower() - - # 如果URL包含视频关键字,认为是视频 - if any(keyword in url_lower for keyword in video_keywords): - return True - - # 检查文件扩展名(传统方法) - path = Path(url.split("?")[0]) # 移除查询参数 - if path.suffix.lower() in self.supported_formats: - return True - - # 对于QQ等特殊平台,URL可能没有扩展名 - # 我们允许这些URL通过,稍后通过HTTP头Content-Type验证 - qq_domains = ["qpic.cn", "gtimg.cn", "qq.com", "tencent.com"] - if any(domain in url_lower for domain in qq_domains): - return True - - return False - except Exception: - # 如果解析失败,默认允许尝试下载(稍后验证) - return True - - def check_file_size(self, content_length: Optional[str]) -> bool: - """检查文件大小是否在允许范围内""" - if content_length is None: - return True # 无法获取大小时允许下载 - - try: - size_bytes = int(content_length) - size_mb = size_bytes / (1024 * 1024) - return size_mb <= self.max_size_mb - except Exception: - return True - - async def download_video(self, url: str, filename: Optional[str] = None) -> Dict[str, Any]: - """ - 下载视频文件 - - Args: - url: 视频URL - filename: 可选的文件名 - - Returns: - dict: 下载结果,包含success、data、filename、error等字段 - """ - try: - logger.info(f"开始下载视频: {url}") - - # 检查URL格式 - if not self.is_video_url(url): - logger.warning(f"URL格式检查失败: {url}") - return {"success": False, "error": "不支持的视频格式", "url": url} - - async with aiohttp.ClientSession() as session: - # 先发送HEAD请求检查文件大小 - try: - async with session.head(url, timeout=aiohttp.ClientTimeout(total=10)) as response: - if response.status != 200: - logger.warning(f"HEAD请求失败,状态码: {response.status}") - else: - content_length = response.headers.get("Content-Length") - if not self.check_file_size(content_length): - return { - "success": False, - "error": f"视频文件过大,超过{self.max_size_mb}MB限制", - "url": url, - } - except Exception as e: - logger.warning(f"HEAD请求失败: {e},继续尝试下载") - - # 下载文件 - async with session.get(url, timeout=aiohttp.ClientTimeout(total=self.download_timeout)) as response: - if response.status != 200: - return {"success": False, "error": f"下载失败,HTTP状态码: {response.status}", "url": url} - - # 检查Content-Type是否为视频 - content_type = response.headers.get("Content-Type", "").lower() - if content_type: - # 检查是否为视频类型 - video_mime_types = [ - "video/", - "application/octet-stream", - "application/x-msvideo", - "video/x-msvideo", - ] - is_video_content = any(mime in content_type for mime in video_mime_types) - - if not is_video_content: - logger.warning(f"Content-Type不是视频格式: {content_type}") - # 如果不是明确的视频类型,但可能是QQ的特殊格式,继续尝试 - if "text/" in content_type or "application/json" in content_type: - return { - "success": False, - "error": f"URL返回的不是视频内容,Content-Type: {content_type}", - "url": url, - } - - # 再次检查Content-Length - content_length = response.headers.get("Content-Length") - if not self.check_file_size(content_length): - return {"success": False, "error": f"视频文件过大,超过{self.max_size_mb}MB限制", "url": url} - - # 读取文件内容 - video_data = await response.read() - - # 检查实际文件大小 - actual_size_mb = len(video_data) / (1024 * 1024) - if actual_size_mb > self.max_size_mb: - return { - "success": False, - "error": f"视频文件过大,实际大小: {actual_size_mb:.2f}MB", - "url": url, - } - - # 确定文件名 - if filename is None: - filename = Path(url.split("?")[0]).name - if not filename or "." not in filename: - filename = "video.mp4" - - logger.info(f"视频下载成功: {filename}, 大小: {actual_size_mb:.2f}MB") - - return { - "success": True, - "data": video_data, - "filename": filename, - "size_mb": actual_size_mb, - "url": url, - } - - except asyncio.TimeoutError: - return {"success": False, "error": "下载超时", "url": url} - except Exception as e: - logger.error(f"下载视频时出错: {e}") - return {"success": False, "error": str(e), "url": url} - - -# 全局实例 -_video_downloader = None - - -def get_video_downloader(max_size_mb: int = 100, download_timeout: int = 60) -> VideoDownloader: - """获取视频下载器实例""" - global _video_downloader - if _video_downloader is None: - _video_downloader = VideoDownloader(max_size_mb, download_timeout) - return _video_downloader diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/websocket_manager.py b/src/plugins/built_in/napcat_adapter_plugin/src/websocket_manager.py deleted file mode 100644 index dd248ea82..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/websocket_manager.py +++ /dev/null @@ -1,163 +0,0 @@ -import asyncio -from typing import Any, Callable, Optional - -import websockets as Server - -from src.common.logger import get_logger -from src.plugin_system.apis import config_api - -logger = get_logger("napcat_adapter") - - -class WebSocketManager: - """WebSocket 连接管理器,支持正向和反向连接""" - - def __init__(self): - self.connection: Optional[Server.ServerConnection] = None - self.server: Optional[Server.WebSocketServer] = None - self.is_running = False - self.reconnect_interval = 5 # 重连间隔(秒) - self.max_reconnect_attempts = 10 # 最大重连次数 - self.plugin_config = None - - async def start_connection( - self, message_handler: Callable[[Server.ServerConnection], Any], plugin_config: dict - ) -> None: - """根据配置启动 WebSocket 连接""" - self.plugin_config = plugin_config - mode = config_api.get_plugin_config(plugin_config, "napcat_server.mode") - - if mode == "reverse": - await self._start_reverse_connection(message_handler) - elif mode == "forward": - await self._start_forward_connection(message_handler) - else: - raise ValueError(f"不支持的连接模式: {mode}") - - async def _start_reverse_connection(self, message_handler: Callable[[Server.ServerConnection], Any]) -> None: - """启动反向连接(作为服务器)""" - host = config_api.get_plugin_config(self.plugin_config, "napcat_server.host") - port = config_api.get_plugin_config(self.plugin_config, "napcat_server.port") - - logger.info(f"正在启动反向连接模式,监听地址: ws://{host}:{port}") - - async def handle_client(websocket, path=None): - self.connection = websocket - logger.info(f"Napcat 客户端已连接: {websocket.remote_address}") - try: - await message_handler(websocket) - except Exception as e: - logger.error(f"处理客户端连接时出错: {e}") - finally: - self.connection = None - logger.info("Napcat 客户端已断开连接") - - self.server = await Server.serve(handle_client, host, port, max_size=2**26) - self.is_running = True - logger.info(f"反向连接服务器已启动,监听地址: ws://{host}:{port}") - - # 保持服务器运行 - await self.server.serve_forever() - - async def _start_forward_connection(self, message_handler: Callable[[Server.ServerConnection], Any]) -> None: - """启动正向连接(作为客户端)""" - url = self._get_forward_url() - logger.info(f"正在启动正向连接模式,目标地址: {url}") - - reconnect_count = 0 - - while reconnect_count < self.max_reconnect_attempts: - try: - logger.info(f"尝试连接到 Napcat 服务器: {url}") - - # 准备连接参数 - connect_kwargs = {"max_size": 2**26} - - # 如果配置了访问令牌,添加到请求头 - access_token = config_api.get_plugin_config(self.plugin_config, "napcat_server.access_token") - if access_token: - connect_kwargs["additional_headers"] = {"Authorization": f"Bearer {access_token}"} - logger.info("已添加访问令牌到连接请求头") - - async with Server.connect(url, **connect_kwargs) as websocket: - self.connection = websocket - self.is_running = True - reconnect_count = 0 # 重置重连计数 - - logger.info(f"成功连接到 Napcat 服务器: {url}") - - try: - await message_handler(websocket) - except Server.exceptions.ConnectionClosed: - logger.warning("与 Napcat 服务器的连接已断开") - except Exception as e: - logger.error(f"处理正向连接时出错: {e}") - finally: - self.connection = None - self.is_running = False - - except ( - Server.exceptions.ConnectionClosed, - Server.exceptions.InvalidMessage, - OSError, - ConnectionRefusedError, - ) as e: - reconnect_count += 1 - logger.warning(f"连接失败 ({reconnect_count}/{self.max_reconnect_attempts}): {e}") - - if reconnect_count < self.max_reconnect_attempts: - logger.info(f"将在 {self.reconnect_interval} 秒后重试连接...") - await asyncio.sleep(self.reconnect_interval) - else: - logger.error("已达到最大重连次数,停止重连") - raise - except Exception as e: - logger.error(f"正向连接时发生未知错误: {e}") - raise - - def _get_forward_url(self) -> str: - """获取正向连接的 URL""" - # 如果配置了完整的 URL,直接使用 - url = config_api.get_plugin_config(self.plugin_config, "napcat_server.url") - if url: - return url - - # 否则根据 host 和 port 构建 URL - host = config_api.get_plugin_config(self.plugin_config, "napcat_server.host") - port = config_api.get_plugin_config(self.plugin_config, "napcat_server.port") - return f"ws://{host}:{port}" - - async def stop_connection(self) -> None: - """停止 WebSocket 连接""" - self.is_running = False - - if self.connection: - try: - await self.connection.close() - logger.info("WebSocket 连接已关闭") - except Exception as e: - logger.error(f"关闭 WebSocket 连接时出错: {e}") - finally: - self.connection = None - - if self.server: - try: - self.server.close() - await self.server.wait_closed() - logger.info("WebSocket 服务器已关闭") - except Exception as e: - logger.error(f"关闭 WebSocket 服务器时出错: {e}") - finally: - self.server = None - - def get_connection(self) -> Optional[Server.ServerConnection]: - """获取当前的 WebSocket 连接""" - return self.connection - - def is_connected(self) -> bool: - """检查是否已连接""" - return self.connection is not None and self.is_running - - -# 全局 WebSocket 管理器实例 -websocket_manager = WebSocketManager() diff --git a/src/plugins/built_in/napcat_adapter_plugin/todo.md b/src/plugins/built_in/napcat_adapter_plugin/todo.md deleted file mode 100644 index 6eff60839..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/todo.md +++ /dev/null @@ -1,89 +0,0 @@ -# TODO List: - -- [x] logger使用主程序的 -- [ ] 使用插件系统的config系统 -- [x] 接收从napcat传递的所有信息 -- [ ] 优化架构,各模块解耦,暴露关键方法用于提供接口 -- [ ] 单独一个模块负责与主程序通信 -- [ ] 使用event系统完善接口api - - ---- -Event分为两种,一种是对外输出的event,由napcat插件自主触发并传递参数,另一种是接收外界输入的event,由外部插件触发并向napcat传递参数 - - -## 例如, - -### 对外输出的event: - -napcat_on_received_text -> (message_seg: Seg) 接受到qq的文字消息,会向handler传递一个Seg -napcat_on_received_face -> (message_seg: Seg) 接受到qq的表情消息,会向handler传递一个Seg -napcat_on_received_reply -> (message_seg: Seg) 接受到qq的回复消息,会向handler传递一个Seg -napcat_on_received_image -> (message_seg: Seg) 接受到qq的图片消息,会向handler传递一个Seg -napcat_on_received_image -> (message_seg: Seg) 接受到qq的图片消息,会向handler传递一个Seg -napcat_on_received_record -> (message_seg: Seg) 接受到qq的语音消息,会向handler传递一个Seg -napcat_on_received_rps -> (message_seg: Seg) 接受到qq的猜拳魔法表情,会向handler传递一个Seg -napcat_on_received_friend_invitation -> (user_id: str) 接受到qq的好友请求,会向handler传递一个user_id -... - -此类event不接受外部插件的触发,只能由napcat插件统一触发。 - -外部插件需要编写handler并订阅此类事件。 -```python -from src.plugin_system.core.event_manager import event_manager -from src.plugin_system.base.base_event import HandlerResult - -class MyEventHandler(BaseEventHandler): - handler_name = "my_handler" - handler_description = "我的自定义事件处理器" - weight = 10 # 权重,越大越先执行 - intercept_message = False # 是否拦截消息 - init_subscribe = ["napcat_on_received_text"] # 初始订阅的事件 - - async def execute(self, params: dict) -> HandlerResult: - """处理事件""" - try: - message = params.get("message_seg") - print(f"收到消息: {message.data}") - - # 业务逻辑处理 - # ... - - return HandlerResult( - success=True, - continue_process=True, # 是否继续让其他处理器处理 - message="处理成功", - handler_name=self.handler_name - ) - except Exception as e: - return HandlerResult( - success=False, - continue_process=True, - message=f"处理失败: {str(e)}", - handler_name=self.handler_name - ) - -``` - -### 接收外界输入的event: - -napcat_kick_group <- (user_id, group_id) 踢出某个群组中的某个用户 -napcat_mute_user <- (user_id, group_id, time) 禁言某个群组中的某个用户 -napcat_unmute_user <- (user_id, group_id) 取消禁言某个群组中的某个用户 -napcat_mute_group <- (user_id, group_id) 禁言某个群组 -napcat_unmute_group <- (user_id, group_id) 取消禁言某个群组 -napcat_add_friend <- (user_id) 向某个用户发出好友请求 -napcat_accept_friend <- (user_id) 接收某个用户的好友请求 -napcat_reject_friend <- (user_id) 拒绝某个用户的好友请求 -... -此类事件只由外部插件触发并传递参数,由napcat完成请求任务。 - -外部插件需要触发此类的event并传递正确的参数。 - -```python -from src.plugin_system.core.event_manager import event_manager - -# 触发事件 -await event_manager.trigger_event("napcat_accept_friend", user_id = 1234123) -``` -