* fix(discord): 修复 WebSocket 连接检测并增强跨平台文件处理

修复 Discord WebSocket 连接检测逻辑,使用正确的属性检查连接状态
为跨平台消息处理添加文件类型支持,并增加详细的调试日志
优化附件处理逻辑,确保所有文件类型都能正确识别和转发

* feat(跨平台): 优化消息处理并添加纯文本提取功能

添加 extract_text_only 函数过滤非文本标记
修改翻译逻辑仅处理纯文本内容
完善附件处理和消息内容拼接
修复仅包含表情时的消息处理问题

* refactor(discord-cross): 使用模块专用日志记录器替换全局日志记录器

将各模块中的全局日志记录器替换为模块专用日志记录器,以提供更清晰的日志来源标识
同时在适配器中添加会话状态检查和重连机制,提升消息发送的可靠性

* feat(翻译): 改进翻译功能,同时显示原文和译文

修改翻译功能,不再替换原文而是同时显示原文和翻译内容,方便用户对照
更新 DeepSeek API 配置为官方地址和模型
优化 Discord 适配器的重连逻辑,直接关闭 WebSocket 触发重连
修复 Discord 频道 ID 转换逻辑,简化处理流程

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

- 新增跨平台配置模型和全局配置支持
- 优化 Discord 适配器的连接管理和错误处理
- 添加 watchdog 和 discord.py 依赖
- 创建 DeepSeek API 配置文档
- 移除重复的同步帮助图片代码
- 改进跨平台插件配置加载逻辑

* fix(jrcd): 修正群组ID检查条件

删除不再使用的示例插件文件
This commit is contained in:
镀铬酸钾
2026-03-23 16:52:15 +08:00
committed by GitHub
parent c810cd5cf9
commit 393227fdd2
11 changed files with 174 additions and 215 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 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: except Exception as e:
retry_count += 1 retry_count += 1
self.logger.error(f"Discord 连接失败: {e}") self.logger.error(f"Discord 连接异常: {e}")
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 self.http.close()
self.clear()
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

@@ -1,38 +0,0 @@
from core.plugin import Plugin, command, on_message
from models.events.message import MessageEvent
from core.permission import Permission
# 插件元信息
__plugin_meta__ = {
"name": "类风格插件示例",
"description": "演示如何使用类风格编写插件",
"usage": "/hello - 打招呼\n/echo <msg> - 复读消息",
}
class MyPlugin(Plugin):
def __init__(self):
super().__init__()
# 可以在这里初始化一些状态
self.count = 0
@command("hello")
async def hello(self, event: MessageEvent, args: list[str]):
self.count += 1
await self.reply(event, f"Hello from class-based plugin! (Called {self.count} times)")
@command("echo", permission=Permission.USER)
async def echo(self, event: MessageEvent, args: list[str]):
if args:
await self.reply(event, " ".join(args))
else:
await self.reply(event, "请输入要复读的内容。")
@on_message()
async def handle_message(self, event: MessageEvent):
# 这是一个通用的消息处理器,会处理所有消息
# 注意:这可能会与命令冲突,通常需要过滤
if "特定关键词" in event.raw_message:
await self.reply(event, "检测到特定关键词!")
# 实例化插件以注册
plugin = MyPlugin()

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: mappings = cross_platform_config.get("mappings", {})
for key, value in mappings.items(): self.CROSS_PLATFORM_MAP.clear()
if isinstance(value, dict) and "qq_group_id" in value:
try: if isinstance(mappings, dict) and mappings:
# 直接将 key 转换为整数 for key, value in mappings.items():
discord_id = int(str(key)) if isinstance(value, dict) and "qq_group_id" in value:
self.CROSS_PLATFORM_MAP[discord_id] = { try:
"qq_group_id": int(value.get("qq_group_id", 0)), # 直接将 key 转换为整数
"name": value.get("name", "") discord_id = int(str(key))
} self.CROSS_PLATFORM_MAP[discord_id] = {
except (ValueError, AttributeError): "qq_group_id": int(value.get("qq_group_id", 0)),
logger.warning(f"[CrossPlatform] 无效的 Discord 频道 ID: {key}") "name": value.get("name", "")
continue }
except (ValueError, AttributeError):
logger.success(f"[CrossPlatform] 配置已重新加载: {len(self.CROSS_PLATFORM_MAP)} 个映射") 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

@@ -129,7 +129,7 @@ async def handle_jrcd_stats(bot: Bot, event: MessageEvent, args: list[str]):
@matcher.command("bbcd") @matcher.command("bbcd")
async def handle_bbcd(bot: Bot, event: MessageEvent, args: list[str]): async def handle_bbcd(bot: Bot, event: MessageEvent, args: list[str]):
if event.id == 831797331: if event.group_id == 831797331:
return None return None
""" """
处理 bbcd 指令,比较两位用户的“长度”。 处理 bbcd 指令,比较两位用户的“长度”。

View File

@@ -1,41 +0,0 @@
from core.plugin import SimplePlugin
from models.events.message import MessageEvent
# 插件元信息
__plugin_meta__ = {
"name": "极简插件示例",
"description": "演示面向新手的极简插件写法",
"usage": "/ping - 测试\n/add <a> <b> - 加法\n/greet <name> - 问候",
}
class MySimplePlugin(SimplePlugin):
async def ping(self, event: MessageEvent):
"""
发送 /ping 即可调用
"""
return "Pong! (来自极简插件)"
async def greet(self, event: MessageEvent, name: str):
"""
发送 /greet Neo 即可调用
"""
return f"你好, {name}!"
async def add(self, event: MessageEvent, a: int, b: int):
"""
发送 /add 10 20 即可调用
自动处理类型转换
"""
return f"{a} + {b} = {a + b}"
async def echo_all(self, event: MessageEvent, msg: str):
"""
只有一个参数时,会自动捕获所有剩余文本
发送 /echo_all 这是一个 测试 消息
msg 将会是 "这是一个 测试 消息"
"""
return f"复读: {msg}"
# 实例化插件以生效
plugin = MySimplePlugin()

View File

@@ -1,88 +0,0 @@
"""
同步/异步函数测试插件
用于演示 SyncHandlerError 异常以及如何将同步函数放入线程池执行。
"""
import time
from typing import Any
from core.managers.command_manager import matcher
from core.utils.executor import run_in_thread_pool
from core.bot import Bot
from core.utils.logger import logger
# 插件元数据
__plugin_meta__ = {
"name": "SyncAsyncTestPlugin",
"description": "用于测试同步/异步函数处理的插件。",
"usage": (
"/test_sync_error - 尝试注册一个同步函数作为异步处理器,会触发错误。\n"
"/test_blocking_task <duration> - 演示将同步阻塞任务放入线程池执行。"
),
}
# --- 示例 1: 触发 SyncHandlerError (此函数不会被成功注册) ---
# 这是一个同步函数,如果直接用 @matcher.message_handler 装饰,
# 并且 event_handler 检查到它是同步的,就会抛出 SyncHandlerError。
# 注意:为了演示错误,我们不会真正注册它,因为注册会失败。
def _sync_function_that_should_fail(bot: Bot, event: Any):
"""
一个同步函数,如果直接作为异步事件处理器注册,会触发 SyncHandlerError。
"""
logger.info("这个同步函数不应该被直接调用。")
return "这是一个同步函数的结果。"
# --- 示例 2: 将同步阻塞任务放入线程池运行 ---
def _blocking_task(duration: int) -> str:
"""
一个模拟耗时操作的同步函数。
Args:
duration (int): 模拟阻塞的秒数。
Returns:
str: 任务完成消息。
"""
logger.info(f"同步阻塞任务开始,持续 {duration} 秒...")
time.sleep(duration)
logger.info("同步阻塞任务结束。")
return f"阻塞任务完成,耗时 {duration} 秒。"
@matcher.message_handler.command("test_blocking_task")
async def test_blocking_task_handler(bot: Bot, event: Any, args: list):
"""
处理 /test_blocking_task 命令,将同步阻塞任务放入线程池执行。
Args:
bot (Bot): 机器人实例。
event (Any): 接收到的事件对象。
args (list): 命令参数列表。
"""
if not args:
await bot.send(event, "请提供阻塞时长,例如:/test_blocking_task 5")
return
try:
duration = int(args[0])
if duration <= 0:
raise ValueError("时长必须是正整数。")
except ValueError:
await bot.send(event, "无效的时长,请提供一个正整数。")
return
await bot.send(event, f"开始执行同步阻塞任务,预计耗时 {duration} 秒...")
# 将同步函数放入线程池执行
result = await run_in_thread_pool(_blocking_task, duration)
await bot.send(event, f"同步阻塞任务已完成:{result}")
# --- 示例 3: 尝试注册一个同步函数作为异步处理器 (会失败) ---
# 这个函数不会被成功注册,因为 event_handler 会检测到它是同步的并抛出 SyncHandlerError。
# 插件管理器会捕获这个错误并跳过加载此插件。
# 为了演示,我们故意尝试注册它。
# @matcher.message_handler.command("test_sync_error")
# def test_sync_error_handler(bot: Bot, event: Any):
# """
# 这个同步函数尝试作为异步处理器注册,会触发 SyncHandlerError。
# """
# logger.error("这个同步函数不应该被直接注册为异步处理器。")
# return "这个消息不应该被看到。"

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