From f8377f547b131bab746035180b854114706df985 Mon Sep 17 00:00:00 2001 From: K2Cr2O1 <2221577113@qq.com> Date: Mon, 23 Mar 2026 16:49:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(cross-platform):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=B7=A8=E5=B9=B3=E5=8F=B0=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8F=8A=E9=85=8D=E7=BD=AE=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增跨平台配置模型和全局配置支持 - 优化 Discord 适配器的连接管理和错误处理 - 添加 watchdog 和 discord.py 依赖 - 创建 DeepSeek API 配置文档 - 移除重复的同步帮助图片代码 - 改进跨平台插件配置加载逻辑 --- DEEPSEEK_API_SETUP.md | 32 +++++ core/config_loader.py | 183 +++++++++++++++++++++++++ plugins/discord-cross/config.py | 100 +++++++++----- src/neobot/adapters/discord_adapter.py | 53 ++++++- 4 files changed, 328 insertions(+), 40 deletions(-) create mode 100644 DEEPSEEK_API_SETUP.md create mode 100644 core/config_loader.py diff --git a/DEEPSEEK_API_SETUP.md b/DEEPSEEK_API_SETUP.md new file mode 100644 index 0000000..27081c7 --- /dev/null +++ b/DEEPSEEK_API_SETUP.md @@ -0,0 +1,32 @@ +# DeepSeek API 配置示例 + +将以下环境变量添加到你的系统环境变量或 .env 文件中: + +```bash +# DeepSeek API Key (从 https://platform.deepseek.com 获取) +DEEPSEEK_API_KEY=sk-你的实际API密钥 + +# DeepSeek API URL (可选,默认为官方 API) +DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions + +# DeepSeek 模型名称 (可选,默认为 deepseek-chat) +DEEPSEEK_MODEL=deepseek-chat +``` + +或者在 Windows 系统中,可以通过以下方式设置环境变量: + +**临时设置(仅当前会话有效):** +```powershell +$env:DEEPSEEK_API_KEY="sk-你的实际API密钥" +$env:DEEPSEEK_API_URL="https://api.deepseek.com/v1/chat/completions" +$env:DEEPSEEK_MODEL="deepseek-chat" +``` + +**永久设置(需要管理员权限):** +```powershell +[Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY", "sk-你的实际API密钥", "User") +[Environment]::SetEnvironmentVariable("DEEPSEEK_API_URL", "https://api.deepseek.com/v1/chat/completions", "User") +[Environment]::SetEnvironmentVariable("DEEPSEEK_MODEL", "deepseek-chat", "User") +``` + +设置完成后,重启终端或 IDE 使环境变量生效。 diff --git a/core/config_loader.py b/core/config_loader.py new file mode 100644 index 0000000..f2b506f --- /dev/null +++ b/core/config_loader.py @@ -0,0 +1,183 @@ +""" +配置加载模块 + +负责读取和解析 config.toml 配置文件,提供全局配置对象。 +""" +from pathlib import Path + +import tomllib +from pydantic import ValidationError +from .config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel, ImageManagerModel, MySQLModel, ReverseWSModel, ThreadingModel, BilibiliModel, LocalFileServerModel, DiscordModel, CrossPlatformModel, LoggingModel +from .utils.logger import ModuleLogger +from .utils.exceptions import ConfigError, ConfigNotFoundError, ConfigValidationError + + +class Config: + """ + 配置加载类,负责读取和解析 config.toml 文件 + """ + + def __init__(self, file_path: str = "config.toml"): + """ + 初始化配置加载器 + + :param file_path: 配置文件路径,默认为 "config.toml" + """ + self.path = Path(file_path) + self._model: ConfigModel + # 创建模块专用日志记录器 + self.logger = ModuleLogger("ConfigLoader") + self.load() + + def load(self): + """ + 加载并验证配置文件 + + :raises ConfigNotFoundError: 如果配置文件不存在 + :raises ConfigValidationError: 如果配置格式不正确 + :raises ConfigError: 如果加载配置时发生其他错误 + """ + if not self.path.exists(): + error = ConfigNotFoundError(message=f"配置文件 {self.path} 未找到!") + self.logger.error(f"配置加载失败: {error.message}") + self.logger.log_custom_exception(error) + raise error + + try: + self.logger.info(f"正在从 {self.path} 加载配置...") + with open(self.path, "rb") as f: + raw_config = tomllib.load(f) + + self._model = ConfigModel(**raw_config) + self.logger.success("配置加载并验证成功!") + + except ValidationError as e: + error_details = [] + for error in e.errors(): + field = " -> ".join(map(str, error["loc"])) + error_msg = f"字段 '{field}': {error['msg']}" + error_details.append(error_msg) + + validation_error = ConfigValidationError( + message="配置验证失败" + ) + validation_error.original_error = e + + self.logger.error("配置验证失败,请检查 `config.toml` 文件中的以下错误:") + for detail in error_details: + self.logger.error(f" - {detail}") + + self.logger.log_custom_exception(validation_error) + raise validation_error + except tomllib.TOMLDecodeError as e: + error = ConfigError( + message=f"TOML解析错误: {str(e)}" + ) + error.original_error = e + self.logger.error(f"加载配置文件时发生TOML解析错误: {error.message}") + self.logger.log_custom_exception(error) + raise error + except Exception as e: + error = ConfigError( + message=f"加载配置文件时发生未知错误: {str(e)}" + ) + error.original_error = e + self.logger.exception(f"加载配置文件时发生未知错误: {error.message}") + self.logger.log_custom_exception(error) + raise error + + # 通过属性访问配置 + @property + def napcat_ws(self) -> NapCatWSModel: + """ + 获取 NapCat WebSocket 配置 + """ + return self._model.napcat_ws + + @property + def bot(self) -> BotModel: + """ + 获取 Bot 基础配置 + """ + return self._model.bot + + @property + def redis(self) -> RedisModel: + """ + 获取 Redis 配置 + """ + return self._model.redis + + @property + def mysql(self) -> MySQLModel: + """ + 获取 MySQL 配置 + """ + return self._model.mysql + + @property + def docker(self) -> DockerModel: + """ + 获取 Docker 配置 + """ + return self._model.docker + + @property + def image_manager(self) -> ImageManagerModel: + """ + 获取图片生成管理器配置 + """ + return self._model.image_manager + + @property + def reverse_ws(self) -> ReverseWSModel: + """ + 获取反向 WebSocket 配置 + """ + return self._model.reverse_ws + + @property + def threading(self) -> ThreadingModel: + """ + 获取线程管理配置 + """ + return self._model.threading + + @property + def bilibili(self) -> BilibiliModel: + """ + 获取 Bilibili 配置 + """ + return self._model.bilibili + + @property + def local_file_server(self) -> LocalFileServerModel: + """ + 获取本地文件服务器配置 + """ + return self._model.local_file_server + + @property + def discord(self) -> DiscordModel: + """ + 获取 Discord 配置 + """ + return self._model.discord + + @property + def cross_platform(self) -> CrossPlatformModel: + """ + 获取跨平台配置 + """ + return self._model.cross_platform + + @property + def logging(self) -> LoggingModel: + """ + 获取日志配置 + """ + return self._model.logging + + +# 实例化全局配置对象 +global_config = Config() diff --git a/plugins/discord-cross/config.py b/plugins/discord-cross/config.py index e1a2153..f4737f6 100644 --- a/plugins/discord-cross/config.py +++ b/plugins/discord-cross/config.py @@ -5,6 +5,7 @@ import os from typing import Dict, Any from core.utils.logger import ModuleLogger +from core.config_loader import global_config # 创建模块专用日志记录器 logger = ModuleLogger("CrossPlatformConfig") @@ -15,50 +16,81 @@ class CrossPlatformConfig: self.CROSS_PLATFORM_CHANNEL = "neobot_cross_platform" self.ENABLE_CROSS_PLATFORM = True - # DeepSeek API 配置 - self.DEEPSEEK_API_KEY = "sk-7b824b05e85445f8a9ceef6c849388a9" - self.DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions" - self.DEEPSEEK_MODEL = "deepseek-chat" + # DeepSeek API 配置 - 从环境变量或配置文件加载 + self.DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") + self.DEEPSEEK_API_URL = os.environ.get("DEEPSEEK_API_URL", "https://api.deepseek.com/v1/chat/completions") + self.DEEPSEEK_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat") # 是否启用翻译功能 self.ENABLE_TRANSLATION = True + + # 从全局配置加载 + self.load_from_global_config() + + def load_from_global_config(self): + """从全局配置加载跨平台配置""" + if global_config and hasattr(global_config, 'cross_platform'): + cross_platform_config = global_config.cross_platform + if cross_platform_config: + self.ENABLE_CROSS_PLATFORM = getattr(cross_platform_config, 'enabled', True) + self.CROSS_PLATFORM_MAP = {} + + # 加载 mappings + if hasattr(cross_platform_config, 'mappings') and cross_platform_config.mappings: + for discord_id, mapping in cross_platform_config.mappings.items(): + if isinstance(mapping, dict): + self.CROSS_PLATFORM_MAP[discord_id] = { + "qq_group_id": int(mapping.get("qq_group_id", 0)), + "name": mapping.get("name", "") + } + elif hasattr(mapping, 'qq_group_id'): + self.CROSS_PLATFORM_MAP[discord_id] = { + "qq_group_id": int(mapping.qq_group_id), + "name": getattr(mapping, 'name', "") + } + logger.success(f"[CrossPlatform] 从全局配置加载了 {len(self.CROSS_PLATFORM_MAP)} 个映射") async def reload(self): """重新加载配置""" try: - config_path = os.path.join(os.path.dirname(__file__), "..", "..", "config.toml") + # 优先使用全局配置 + self.load_from_global_config() - if os.path.exists(config_path): - try: - import tomllib - except ImportError: - import tomli as tomllib + # 如果全局配置不可用,尝试从文件加载 + if not self.CROSS_PLATFORM_MAP: + config_path = os.path.join(os.path.dirname(__file__), "..", "..", "config.toml") - with open(config_path, "rb") as f: - config_data = tomllib.load(f) + if os.path.exists(config_path): + try: + import tomllib + except ImportError: + import tomli as tomllib - cross_platform_config = config_data.get("cross_platform", {}) - self.ENABLE_CROSS_PLATFORM = cross_platform_config.get("enabled", True) - - # 重新加载映射配置 - mappings = cross_platform_config.get("mappings", {}) - self.CROSS_PLATFORM_MAP.clear() - - if isinstance(mappings, dict) and mappings: - for key, value in mappings.items(): - if isinstance(value, dict) and "qq_group_id" in value: - try: - # 直接将 key 转换为整数 - discord_id = int(str(key)) - self.CROSS_PLATFORM_MAP[discord_id] = { - "qq_group_id": int(value.get("qq_group_id", 0)), - "name": value.get("name", "") - } - except (ValueError, AttributeError): - logger.warning(f"[CrossPlatform] 无效的 Discord 频道 ID: {key}") - continue - - logger.success(f"[CrossPlatform] 配置已重新加载: {len(self.CROSS_PLATFORM_MAP)} 个映射") + with open(config_path, "rb") as f: + config_data = tomllib.load(f) + + cross_platform_config = config_data.get("cross_platform", {}) + self.ENABLE_CROSS_PLATFORM = cross_platform_config.get("enabled", True) + + # 重新加载映射配置 + mappings = cross_platform_config.get("mappings", {}) + self.CROSS_PLATFORM_MAP.clear() + + if isinstance(mappings, dict) and mappings: + for key, value in mappings.items(): + if isinstance(value, dict) and "qq_group_id" in value: + try: + # 直接将 key 转换为整数 + discord_id = int(str(key)) + self.CROSS_PLATFORM_MAP[discord_id] = { + "qq_group_id": int(value.get("qq_group_id", 0)), + "name": value.get("name", "") + } + except (ValueError, AttributeError): + logger.warning(f"[CrossPlatform] 无效的 Discord 频道 ID: {key}") + continue + + logger.success(f"[CrossPlatform] 配置已重新加载: {len(self.CROSS_PLATFORM_MAP)} 个映射") except Exception as e: logger.error(f"[CrossPlatform] 重新加载配置失败: {e}") diff --git a/src/neobot/adapters/discord_adapter.py b/src/neobot/adapters/discord_adapter.py index f4c94b4..172d435 100644 --- a/src/neobot/adapters/discord_adapter.py +++ b/src/neobot/adapters/discord_adapter.py @@ -96,7 +96,7 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object): return try: - channel_name = "neobot_discord_send" + channel_name = "neobot_cross_platform" pubsub = redis_manager.redis.pubsub() await pubsub.subscribe(channel_name) @@ -319,23 +319,64 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object): except asyncio.CancelledError: self.logger.info("连接被取消") break + except discord.ConnectionClosed as e: + retry_count += 1 + self.logger.warning(f"Discord 连接关闭: code={e.code}, reason={e.reason}") + + # 如果是正常关闭,不计入重连次数 + if e.code == 1000: + self.logger.info("连接正常关闭,等待重新连接...") + continue + + if max_retries != -1 and retry_count >= max_retries: + self.logger.error(f"已达到最大重连次数 ({max_retries}),停止重连") + break + + self.logger.info(f"将在 {retry_delay} 秒后重连 ({retry_count}/{max_retries if max_retries != -1 else '无限'})...") + await self._cleanup_connection() + await asyncio.sleep(retry_delay) except Exception as e: retry_count += 1 - self.logger.error(f"Discord 连接失败: {e}") + self.logger.error(f"Discord 连接异常: {e}") if max_retries != -1 and retry_count >= max_retries: self.logger.error(f"已达到最大重连次数 ({max_retries}),停止重连") break self.logger.info(f"将在 {retry_delay} 秒后重连 ({retry_count}/{max_retries if max_retries != -1 else '无限'})...") - # 清理旧的连接状态 - if hasattr(self, 'http') and self.http: - await self.http.close() - self.clear() + await self._cleanup_connection() await asyncio.sleep(retry_delay) self.logger.info("Discord 客户端已停止") + async def _cleanup_connection(self): + """ + 清理旧的连接状态 + """ + try: + # 停止心跳任务 + if hasattr(self, 'heartbeat_task') and not self.heartbeat_task.done(): + self.heartbeat_task.cancel() + try: + await self.heartbeat_task + except asyncio.CancelledError: + pass + except Exception as e: + self.logger.error(f"清理心跳任务时出错: {e}") + + try: + # 清理 HTTP 连接 + if hasattr(self, 'http') and self.http: + await self.http.close() + except Exception as e: + self.logger.error(f"清理 HTTP 连接时出错: {e}") + + try: + # 清理客户端状态 + self.clear() + except Exception as e: + self.logger.error(f"清理客户端状态时出错: {e}") + async def start_heartbeat(self, interval: int = 30): """ 启动心跳机制,定期检查连接状态