feat(cross-platform): 添加跨平台功能支持及配置优化

- 新增跨平台配置模型和全局配置支持
- 优化 Discord 适配器的连接管理和错误处理
- 添加 watchdog 和 discord.py 依赖
- 创建 DeepSeek API 配置文档
- 移除重复的同步帮助图片代码
- 改进跨平台插件配置加载逻辑
This commit is contained in:
2026-03-23 16:49:38 +08:00
parent d96b4b228d
commit e8c422f5ee
7 changed files with 173 additions and 47 deletions

32
DEEPSEEK_API_SETUP.md Normal file
View File

@@ -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 使环境变量生效。

View File

@@ -96,7 +96,7 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
return return
try: try:
channel_name = "neobot_discord_send" channel_name = "neobot_cross_platform"
pubsub = redis_manager.redis.pubsub() pubsub = redis_manager.redis.pubsub()
await pubsub.subscribe(channel_name) await pubsub.subscribe(channel_name)
@@ -319,23 +319,64 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
except asyncio.CancelledError: except asyncio.CancelledError:
self.logger.info("连接被取消") self.logger.info("连接被取消")
break break
except Exception as e: except discord.ConnectionClosed as e:
retry_count += 1 retry_count += 1
self.logger.error(f"Discord 连接失败: {e}") 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: if max_retries != -1 and retry_count >= max_retries:
self.logger.error(f"已达到最大重连次数 ({max_retries}),停止重连") self.logger.error(f"已达到最大重连次数 ({max_retries}),停止重连")
break break
self.logger.info(f"将在 {retry_delay} 秒后重连 ({retry_count}/{max_retries if max_retries != -1 else '无限'})...") self.logger.info(f"将在 {retry_delay} 秒后重连 ({retry_count}/{max_retries if max_retries != -1 else '无限'})...")
# 清理旧的连接状态 await self._cleanup_connection()
if hasattr(self, 'http') and self.http: await asyncio.sleep(retry_delay)
await self.http.close() except Exception as e:
self.clear() retry_count += 1
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 '无限'})...")
await self._cleanup_connection()
await asyncio.sleep(retry_delay) await asyncio.sleep(retry_delay)
self.logger.info("Discord 客户端已停止") 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): async def start_heartbeat(self, interval: int = 30):
""" """
启动心跳机制,定期检查连接状态 启动心跳机制,定期检查连接状态

View File

@@ -7,7 +7,7 @@ from pathlib import Path
import tomllib import tomllib
from pydantic import ValidationError from pydantic import ValidationError
from .config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel, ImageManagerModel, MySQLModel, ReverseWSModel, ThreadingModel, BilibiliModel, LocalFileServerModel, DiscordModel, LoggingModel 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.logger import ModuleLogger
from .utils.exceptions import ConfigError, ConfigNotFoundError, ConfigValidationError from .utils.exceptions import ConfigError, ConfigNotFoundError, ConfigValidationError
@@ -164,6 +164,13 @@ class Config:
""" """
return self._model.discord return self._model.discord
@property
def cross_platform(self) -> CrossPlatformModel:
"""
获取跨平台配置
"""
return self._model.cross_platform
@property @property
def logging(self) -> LoggingModel: def logging(self) -> LoggingModel:
""" """

View File

@@ -13,7 +13,7 @@ class NapCatWSModel(BaseModel):
对应 `config.toml` 中的 `[napcat_ws]` 配置块。 对应 `config.toml` 中的 `[napcat_ws]` 配置块。
""" """
uri: str uri: str
token: str token: str = ""
reconnect_interval: int = 5 reconnect_interval: int = 5
@@ -117,6 +117,22 @@ class DiscordModel(BaseModel):
proxy_type: str = "http" proxy_type: str = "http"
class CrossPlatformMapping(BaseModel):
"""
跨平台映射配置
"""
qq_group_id: int
name: str
class CrossPlatformModel(BaseModel):
"""
对应 `config.toml` 中的 `[cross_platform]` 配置块。
"""
enabled: bool = False
mappings: Optional[dict[int, CrossPlatformMapping]] = None
class LoggingModel(BaseModel): class LoggingModel(BaseModel):
""" """
对应 `config.toml` 中的 `[logging]` 配置块。 对应 `config.toml` 中的 `[logging]` 配置块。
@@ -141,6 +157,7 @@ class ConfigModel(BaseModel):
bilibili: BilibiliModel = Field(default_factory=BilibiliModel) bilibili: BilibiliModel = Field(default_factory=BilibiliModel)
local_file_server: LocalFileServerModel = Field(default_factory=LocalFileServerModel) local_file_server: LocalFileServerModel = Field(default_factory=LocalFileServerModel)
discord: DiscordModel = Field(default_factory=DiscordModel) discord: DiscordModel = Field(default_factory=DiscordModel)
cross_platform: CrossPlatformModel = Field(default_factory=CrossPlatformModel)
logging: LoggingModel = Field(default_factory=LoggingModel) logging: LoggingModel = Field(default_factory=LoggingModel)

View File

@@ -120,16 +120,11 @@ async def main():
# 同步帮助图片 # 同步帮助图片
await matcher.sync_help_pic() await matcher.sync_help_pic()
# 同步帮助图片
await matcher.sync_help_pic()
# 初始化权限管理器(包含了管理员管理功能) # 初始化权限管理器(包含了管理员管理功能)
await permission_manager.initialize() await permission_manager.initialize()
# 初始化浏览器管理器 (使用页面池) # 初始化浏览器管理器 (使用页面池)
await browser_manager.init_pool(size=3) await browser_manager.init_pool(size=3)
# 启动反向 WebSocket 服务端(如果启用)
if config.reverse_ws.enabled: if config.reverse_ws.enabled:
logger.info("正在启动反向 WebSocket 服务端...") logger.info("正在启动反向 WebSocket 服务端...")
asyncio.create_task(reverse_ws_manager.start( asyncio.create_task(reverse_ws_manager.start(

View File

@@ -5,6 +5,7 @@
import os import os
from typing import Dict, Any from typing import Dict, Any
from core.utils.logger import ModuleLogger from core.utils.logger import ModuleLogger
from core.config_loader import global_config
# 创建模块专用日志记录器 # 创建模块专用日志记录器
logger = ModuleLogger("CrossPlatformConfig") logger = ModuleLogger("CrossPlatformConfig")
@@ -15,50 +16,81 @@ class CrossPlatformConfig:
self.CROSS_PLATFORM_CHANNEL = "neobot_cross_platform" self.CROSS_PLATFORM_CHANNEL = "neobot_cross_platform"
self.ENABLE_CROSS_PLATFORM = True self.ENABLE_CROSS_PLATFORM = True
# DeepSeek API 配置 # DeepSeek API 配置 - 从环境变量或配置文件加载
self.DEEPSEEK_API_KEY = "sk-7b824b05e85445f8a9ceef6c849388a9" self.DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
self.DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions" self.DEEPSEEK_API_URL = os.environ.get("DEEPSEEK_API_URL", "https://api.deepseek.com/v1/chat/completions")
self.DEEPSEEK_MODEL = "deepseek-chat" self.DEEPSEEK_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
# 是否启用翻译功能 # 是否启用翻译功能
self.ENABLE_TRANSLATION = True 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): async def reload(self):
"""重新加载配置""" """重新加载配置"""
try: try:
config_path = os.path.join(os.path.dirname(__file__), "..", "..", "config.toml") # 优先使用全局配置
self.load_from_global_config()
if os.path.exists(config_path): # 如果全局配置不可用,尝试从文件加载
try: if not self.CROSS_PLATFORM_MAP:
import tomllib config_path = os.path.join(os.path.dirname(__file__), "..", "..", "config.toml")
except ImportError:
import tomli as tomllib
with open(config_path, "rb") as f: if os.path.exists(config_path):
config_data = tomllib.load(f) try:
import tomllib
except ImportError:
import tomli as tomllib
cross_platform_config = config_data.get("cross_platform", {}) with open(config_path, "rb") as f:
self.ENABLE_CROSS_PLATFORM = cross_platform_config.get("enabled", True) config_data = tomllib.load(f)
# 重新加载映射配置 cross_platform_config = config_data.get("cross_platform", {})
mappings = cross_platform_config.get("mappings", {}) self.ENABLE_CROSS_PLATFORM = cross_platform_config.get("enabled", True)
self.CROSS_PLATFORM_MAP.clear()
if isinstance(mappings, dict) and mappings: # 重新加载映射配置
for key, value in mappings.items(): mappings = cross_platform_config.get("mappings", {})
if isinstance(value, dict) and "qq_group_id" in value: self.CROSS_PLATFORM_MAP.clear()
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)} 个映射") 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: except Exception as e:
logger.error(f"[CrossPlatform] 重新加载配置失败: {e}") logger.error(f"[CrossPlatform] 重新加载配置失败: {e}")

View File

@@ -85,6 +85,7 @@ sympy==1.14.0
trove_classifiers==2026.1.14.14 trove_classifiers==2026.1.14.14
urllib3_secure_extra==0.1.0 urllib3_secure_extra==0.1.0
uvloop==0.22.1 uvloop==0.22.1
watchdog==6.0.0
websocket_client==1.9.0 websocket_client==1.9.0
Werkzeug==3.1.6 Werkzeug==3.1.6
winloop==0.5.0 winloop==0.5.0
@@ -92,3 +93,4 @@ wmi==1.5.1
xmlrpclib==1.0.1 xmlrpclib==1.0.1
xx==3.3.2 xx==3.3.2
zope==5.13 zope==5.13
discord.py==2.3.2