fix(discord): 修复WebSocket连接检查并添加错误日志
refactor(config): 更新配置文件的网络和认证信息 feat(cross-platform): 为跨平台消息处理添加异常捕获和日志
This commit is contained in:
@@ -88,6 +88,9 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
|
||||
await matcher.handle_event(mock_event.bot, mock_event)
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理 Discord 消息时发生异常: {e}")
|
||||
# 记录详细的异常信息
|
||||
import traceback
|
||||
self.logger.error(f"异常堆栈: {traceback.format_exc()}")
|
||||
|
||||
async def start_redis_subscription(self):
|
||||
"""启动 Redis 订阅以处理跨平台消息发送"""
|
||||
@@ -96,7 +99,7 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
|
||||
return
|
||||
|
||||
try:
|
||||
channel_name = "neobot_cross_platform"
|
||||
channel_name = "neobot_discord_send"
|
||||
pubsub = redis_manager.redis.pubsub()
|
||||
await pubsub.subscribe(channel_name)
|
||||
|
||||
@@ -386,16 +389,20 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
|
||||
"""
|
||||
self.logger.info(f"心跳机制已启动,间隔: {interval}秒")
|
||||
|
||||
while self.is_closed() is False:
|
||||
while not self.is_closed():
|
||||
try:
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
# discord.py 的 ws 对象是 DiscordWebSocket,它没有 closed 属性
|
||||
# 我们可以通过检查 self.is_closed() 或者 ws.open 来判断
|
||||
if self.ws is not None and not getattr(self.ws, 'open', True):
|
||||
self.logger.warning("检测到 WebSocket 连接已关闭,触发重连...")
|
||||
await self.ws.close(4000)
|
||||
break
|
||||
# 检查 WebSocket 连接状态
|
||||
if self.ws is not None:
|
||||
# 正确检查 WebSocket 状态
|
||||
if not getattr(self.ws, 'open', False):
|
||||
self.logger.warning("检测到 WebSocket 连接已关闭,触发重连...")
|
||||
try:
|
||||
await self.ws.close(code=4000)
|
||||
except Exception as close_error:
|
||||
self.logger.error(f"关闭 WebSocket 连接时出错: {close_error}")
|
||||
break
|
||||
|
||||
self.logger.debug(f"心跳正常: {self.user}")
|
||||
|
||||
|
||||
@@ -84,117 +84,117 @@ class DiscordBotWrapper:
|
||||
content = ""
|
||||
files = []
|
||||
|
||||
for node in nodes:
|
||||
if node.get("type") == "node":
|
||||
node_data = node.get("data", {})
|
||||
node_content = node_data.get("content", [])
|
||||
|
||||
if isinstance(node_content, str):
|
||||
import re
|
||||
cq_pattern = r'\[CQ:([^,]+)(?:,([^\]]+))?\]'
|
||||
matches = list(re.finditer(cq_pattern, node_content))
|
||||
|
||||
if not matches:
|
||||
content += f"{node_content}\n"
|
||||
else:
|
||||
last_end = 0
|
||||
for match in matches:
|
||||
if match.start() > last_end:
|
||||
content += node_content[last_end:match.start()]
|
||||
|
||||
cq_type = match.group(1)
|
||||
cq_params_str = match.group(2) or ""
|
||||
|
||||
params = {}
|
||||
if cq_params_str:
|
||||
for param in cq_params_str.split(','):
|
||||
if '=' in param:
|
||||
k, v = param.split('=', 1)
|
||||
params[k] = v
|
||||
|
||||
if cq_type in ("image", "video", "record"):
|
||||
file_url = params.get("url") or params.get("file")
|
||||
if file_url:
|
||||
if str(file_url).startswith("http"):
|
||||
content += f"\n{file_url}\n"
|
||||
elif str(file_url).startswith("base64://"):
|
||||
import base64
|
||||
import io
|
||||
b64_data = str(file_url)[9:]
|
||||
if b64_data.startswith("data:image") or b64_data.startswith("data:audio") or b64_data.startswith("data:video"):
|
||||
b64_data = b64_data.split(",", 1)[1]
|
||||
try:
|
||||
file_bytes = base64.b64decode(b64_data)
|
||||
filename = "file.png" if cq_type == "image" else ("file.mp4" if cq_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Base64 文件失败: {e}")
|
||||
else:
|
||||
try:
|
||||
files.append(discord.File(file_url))
|
||||
except Exception as e:
|
||||
logger.error(f"无法读取本地文件 {file_url}: {e}")
|
||||
elif cq_type == "face":
|
||||
# QQ 表情,简单转为文本
|
||||
face_id = params.get("id")
|
||||
content += f"[表情:{face_id}]"
|
||||
elif cq_type == "at":
|
||||
qq_id = params.get("qq")
|
||||
if qq_id == "all":
|
||||
content += "@everyone "
|
||||
else:
|
||||
content += f"<@{qq_id}> "
|
||||
|
||||
last_end = match.end()
|
||||
|
||||
if last_end < len(node_content):
|
||||
content += node_content[last_end:]
|
||||
content += "\n"
|
||||
elif isinstance(node_content, list):
|
||||
for seg in node_content:
|
||||
if isinstance(seg, dict):
|
||||
seg_type = seg.get("type")
|
||||
seg_data = seg.get("data", {})
|
||||
|
||||
if seg_type == "text":
|
||||
content += seg_data.get("text", "")
|
||||
elif seg_type in ("image", "video", "record"):
|
||||
file_url = seg_data.get("url") or seg_data.get("file")
|
||||
if file_url:
|
||||
if isinstance(file_url, bytes):
|
||||
import io
|
||||
try:
|
||||
filename = "file.png" if seg_type == "image" else ("file.mp4" if seg_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_url), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 bytes 文件失败: {e}")
|
||||
elif str(file_url).startswith("http"):
|
||||
content += f"\n{file_url}\n"
|
||||
elif str(file_url).startswith("base64://") or "data:image" in str(file_url) or "data:audio" in str(file_url) or "data:video" in str(file_url):
|
||||
import base64
|
||||
import io
|
||||
b64_data = str(file_url)
|
||||
if b64_data.startswith("base64://"):
|
||||
b64_data = b64_data[9:]
|
||||
if b64_data.startswith("data:image") or b64_data.startswith("data:audio") or b64_data.startswith("data:video"):
|
||||
b64_data = b64_data.split(",", 1)[1]
|
||||
try:
|
||||
file_bytes = base64.b64decode(b64_data)
|
||||
filename = "file.png" if seg_type == "image" else ("file.mp4" if seg_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Base64 文件失败: {e}")
|
||||
else:
|
||||
try:
|
||||
files.append(discord.File(file_url))
|
||||
except Exception as e:
|
||||
logger.error(f"无法读取本地文件 {file_url}: {e}")
|
||||
elif seg_type == "face":
|
||||
face_id = seg_data.get("id")
|
||||
content += f"[表情:{face_id}]"
|
||||
content += "\n"
|
||||
|
||||
try:
|
||||
for node in nodes:
|
||||
if node.get("type") == "node":
|
||||
node_data = node.get("data", {})
|
||||
node_content = node_data.get("content", [])
|
||||
|
||||
if isinstance(node_content, str):
|
||||
import re
|
||||
cq_pattern = r'\[CQ:([^,]+)(?:,([^\]]+))?\]'
|
||||
matches = list(re.finditer(cq_pattern, node_content))
|
||||
|
||||
if not matches:
|
||||
content += f"{node_content}\n"
|
||||
else:
|
||||
last_end = 0
|
||||
for match in matches:
|
||||
if match.start() > last_end:
|
||||
content += node_content[last_end:match.start()]
|
||||
|
||||
cq_type = match.group(1)
|
||||
cq_params_str = match.group(2) or ""
|
||||
|
||||
params = {}
|
||||
if cq_params_str:
|
||||
for param in cq_params_str.split(','):
|
||||
if '=' in param:
|
||||
k, v = param.split('=', 1)
|
||||
params[k] = v
|
||||
|
||||
if cq_type in ("image", "video", "record"):
|
||||
file_url = params.get("url") or params.get("file")
|
||||
if file_url:
|
||||
if str(file_url).startswith("http"):
|
||||
content += f"\n{file_url}\n"
|
||||
elif str(file_url).startswith("base64://"):
|
||||
import base64
|
||||
import io
|
||||
b64_data = str(file_url)[9:]
|
||||
if b64_data.startswith("data:image") or b64_data.startswith("data:audio") or b64_data.startswith("data:video"):
|
||||
b64_data = b64_data.split(",", 1)[1]
|
||||
try:
|
||||
file_bytes = base64.b64decode(b64_data)
|
||||
filename = "file.png" if cq_type == "image" else ("file.mp4" if cq_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Base64 文件失败: {e}")
|
||||
else:
|
||||
try:
|
||||
files.append(discord.File(file_url))
|
||||
except Exception as e:
|
||||
logger.error(f"无法读取本地文件 {file_url}: {e}")
|
||||
elif cq_type == "face":
|
||||
# QQ 表情,简单转为文本
|
||||
face_id = params.get("id")
|
||||
content += f"[表情:{face_id}]"
|
||||
elif cq_type == "at":
|
||||
qq_id = params.get("qq")
|
||||
if qq_id == "all":
|
||||
content += "@everyone "
|
||||
else:
|
||||
content += f"<@{qq_id}> "
|
||||
|
||||
last_end = match.end()
|
||||
|
||||
if last_end < len(node_content):
|
||||
content += node_content[last_end:]
|
||||
content += "\n"
|
||||
elif isinstance(node_content, list):
|
||||
for seg in node_content:
|
||||
if isinstance(seg, dict):
|
||||
seg_type = seg.get("type")
|
||||
seg_data = seg.get("data", {})
|
||||
|
||||
if seg_type == "text":
|
||||
content += seg_data.get("text", "")
|
||||
elif seg_type in ("image", "video", "record"):
|
||||
file_url = seg_data.get("url") or seg_data.get("file")
|
||||
if file_url:
|
||||
if isinstance(file_url, bytes):
|
||||
import io
|
||||
try:
|
||||
filename = "file.png" if seg_type == "image" else ("file.mp4" if seg_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_url), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 bytes 文件失败: {e}")
|
||||
elif str(file_url).startswith("http"):
|
||||
content += f"\n{file_url}\n"
|
||||
elif str(file_url).startswith("base64://") or "data:image" in str(file_url) or "data:audio" in str(file_url) or "data:video" in str(file_url):
|
||||
import base64
|
||||
import io
|
||||
b64_data = str(file_url)
|
||||
if b64_data.startswith("base64://"):
|
||||
b64_data = b64_data[9:]
|
||||
if b64_data.startswith("data:image") or b64_data.startswith("data:audio") or b64_data.startswith("data:video"):
|
||||
b64_data = b64_data.split(",", 1)[1]
|
||||
try:
|
||||
file_bytes = base64.b64decode(b64_data)
|
||||
filename = "file.png" if seg_type == "image" else ("file.mp4" if seg_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Base64 文件失败: {e}")
|
||||
else:
|
||||
try:
|
||||
files.append(discord.File(file_url))
|
||||
except Exception as e:
|
||||
logger.error(f"无法读取本地文件 {file_url}: {e}")
|
||||
elif seg_type == "face":
|
||||
face_id = seg_data.get("id")
|
||||
content += f"[表情:{face_id}]"
|
||||
content += "\n"
|
||||
|
||||
if content or files:
|
||||
# target is usually event, we can use event.bot.send
|
||||
if isinstance(target, GroupMessageEvent):
|
||||
@@ -209,6 +209,8 @@ class DiscordBotWrapper:
|
||||
await user.dm_channel.send(content=content, files=files if files else None)
|
||||
except Exception as e:
|
||||
logger.error(f"发送 Discord 合并转发消息失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"异常堆栈: {traceback.format_exc()}")
|
||||
|
||||
class DiscordToOneBotConverter:
|
||||
"""
|
||||
@@ -416,45 +418,105 @@ class DiscordToOneBotConverter:
|
||||
content = ""
|
||||
files = []
|
||||
|
||||
# 统一转换为列表处理
|
||||
if not isinstance(message, list):
|
||||
message = [message]
|
||||
|
||||
import re
|
||||
|
||||
for segment in message:
|
||||
if isinstance(segment, str):
|
||||
# 尝试解析 CQ 码
|
||||
cq_pattern = r'\[CQ:([^,]+)(?:,([^\]]+))?\]'
|
||||
matches = list(re.finditer(cq_pattern, segment))
|
||||
try:
|
||||
# 统一转换为列表处理
|
||||
if not isinstance(message, list):
|
||||
message = [message]
|
||||
|
||||
if not matches:
|
||||
content += segment
|
||||
continue
|
||||
import re
|
||||
|
||||
for segment in message:
|
||||
if isinstance(segment, str):
|
||||
# 尝试解析 CQ 码
|
||||
cq_pattern = r'\[CQ:([^,]+)(?:,([^\]]+))?\]'
|
||||
matches = list(re.finditer(cq_pattern, segment))
|
||||
|
||||
last_end = 0
|
||||
for match in matches:
|
||||
# 添加 CQ 码之前的纯文本
|
||||
if match.start() > last_end:
|
||||
content += segment[last_end:match.start()]
|
||||
if not matches:
|
||||
content += segment
|
||||
continue
|
||||
|
||||
cq_type = match.group(1)
|
||||
cq_params_str = match.group(2) or ""
|
||||
|
||||
# 解析参数
|
||||
params = {}
|
||||
if cq_params_str:
|
||||
for param in cq_params_str.split(','):
|
||||
if '=' in param:
|
||||
k, v = param.split('=', 1)
|
||||
params[k] = v
|
||||
last_end = 0
|
||||
for match in matches:
|
||||
# 添加 CQ 码之前的纯文本
|
||||
if match.start() > last_end:
|
||||
content += segment[last_end:match.start()]
|
||||
|
||||
cq_type = match.group(1)
|
||||
cq_params_str = match.group(2) or ""
|
||||
|
||||
# 解析参数
|
||||
params = {}
|
||||
if cq_params_str:
|
||||
for param in cq_params_str.split(','):
|
||||
if '=' in param:
|
||||
k, v = param.split('=', 1)
|
||||
params[k] = v
|
||||
|
||||
if cq_type in ("image", "video", "record"):
|
||||
file_url = params.get("url") or params.get("file")
|
||||
if file_url:
|
||||
if str(file_url).startswith("http"):
|
||||
content += f"\n{file_url}"
|
||||
elif str(file_url).startswith("base64://") or "data:image" in str(file_url) or "data:audio" in str(file_url) or "data:video" in str(file_url):
|
||||
import base64
|
||||
import io
|
||||
b64_data = str(file_url)
|
||||
if b64_data.startswith("base64://"):
|
||||
b64_data = b64_data[9:]
|
||||
if b64_data.startswith("data:image") or b64_data.startswith("data:audio") or b64_data.startswith("data:video"):
|
||||
b64_data = b64_data.split(",", 1)[1]
|
||||
try:
|
||||
file_bytes = base64.b64decode(b64_data)
|
||||
filename = "file.png" if cq_type == "image" else ("file.mp4" if cq_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Base64 文件失败: {e}")
|
||||
else:
|
||||
try:
|
||||
files.append(discord.File(file_url))
|
||||
except Exception as e:
|
||||
logger.error(f"无法读取本地文件 {file_url}: {e}")
|
||||
elif cq_type == "face":
|
||||
face_id = params.get("id")
|
||||
content += f"[表情:{face_id}]"
|
||||
elif cq_type == "at":
|
||||
qq_id = params.get("qq")
|
||||
if qq_id == "all":
|
||||
content += "@everyone "
|
||||
else:
|
||||
content += f"<@{qq_id}> "
|
||||
|
||||
if cq_type in ("image", "video", "record"):
|
||||
file_url = params.get("url") or params.get("file")
|
||||
last_end = match.end()
|
||||
|
||||
# 添加最后一个 CQ 码之后的纯文本
|
||||
if last_end < len(segment):
|
||||
content += segment[last_end:]
|
||||
|
||||
elif isinstance(segment, OneBotMessageSegment):
|
||||
# 解析 OneBot 的 MessageSegment
|
||||
seg_type = segment.type
|
||||
seg_data = segment.data
|
||||
|
||||
if seg_type == "text":
|
||||
content += seg_data.get("text", "")
|
||||
elif seg_type in ("image", "video", "record"):
|
||||
# OneBot 的图片/视频/语音通常有 file (URL或本地路径) 或 url 字段
|
||||
file_url = seg_data.get("url") or seg_data.get("file")
|
||||
|
||||
if file_url:
|
||||
if str(file_url).startswith("http"):
|
||||
# 处理 bytes 类型
|
||||
if isinstance(file_url, bytes):
|
||||
import io
|
||||
try:
|
||||
filename = "file.png" if seg_type == "image" else ("file.mp4" if seg_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_url), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 bytes 文件失败: {e}")
|
||||
elif str(file_url).startswith("http"):
|
||||
# 如果是网络 URL,直接拼接到文本中,Discord 会自动解析预览
|
||||
content += f"\n{file_url}"
|
||||
elif str(file_url).startswith("base64://") or "data:image" in str(file_url) or "data:audio" in str(file_url) or "data:video" in str(file_url):
|
||||
# 处理 Base64 文件 (需要解码并作为文件上传)
|
||||
import base64
|
||||
import io
|
||||
b64_data = str(file_url)
|
||||
@@ -464,91 +526,31 @@ class DiscordToOneBotConverter:
|
||||
b64_data = b64_data.split(",", 1)[1]
|
||||
try:
|
||||
file_bytes = base64.b64decode(b64_data)
|
||||
filename = "file.png" if cq_type == "image" else ("file.mp4" if cq_type == "video" else "file.ogg")
|
||||
filename = "file.png" if seg_type == "image" else ("file.mp4" if seg_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Base64 文件失败: {e}")
|
||||
else:
|
||||
# 假设是本地文件路径
|
||||
try:
|
||||
files.append(discord.File(file_url))
|
||||
except Exception as e:
|
||||
logger.error(f"无法读取本地文件 {file_url}: {e}")
|
||||
elif cq_type == "face":
|
||||
face_id = params.get("id")
|
||||
elif seg_type == "face":
|
||||
face_id = seg_data.get("id")
|
||||
content += f"[表情:{face_id}]"
|
||||
elif cq_type == "at":
|
||||
qq_id = params.get("qq")
|
||||
elif seg_type == "at":
|
||||
qq_id = seg_data.get("qq")
|
||||
if qq_id == "all":
|
||||
content += "@everyone "
|
||||
else:
|
||||
# 尝试将 QQ 号映射回 Discord ID (这里简单处理,直接拼接)
|
||||
content += f"<@{qq_id}> "
|
||||
|
||||
last_end = match.end()
|
||||
|
||||
# 添加最后一个 CQ 码之后的纯文本
|
||||
if last_end < len(segment):
|
||||
content += segment[last_end:]
|
||||
|
||||
elif isinstance(segment, OneBotMessageSegment):
|
||||
# 解析 OneBot 的 MessageSegment
|
||||
seg_type = segment.type
|
||||
seg_data = segment.data
|
||||
|
||||
if seg_type == "text":
|
||||
content += seg_data.get("text", "")
|
||||
elif seg_type in ("image", "video", "record"):
|
||||
# OneBot 的图片/视频/语音通常有 file (URL或本地路径) 或 url 字段
|
||||
file_url = seg_data.get("url") or seg_data.get("file")
|
||||
|
||||
if file_url:
|
||||
# 处理 bytes 类型
|
||||
if isinstance(file_url, bytes):
|
||||
import io
|
||||
try:
|
||||
filename = "file.png" if seg_type == "image" else ("file.mp4" if seg_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_url), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 bytes 文件失败: {e}")
|
||||
elif str(file_url).startswith("http"):
|
||||
# 如果是网络 URL,直接拼接到文本中,Discord 会自动解析预览
|
||||
content += f"\n{file_url}"
|
||||
elif str(file_url).startswith("base64://") or "data:image" in str(file_url) or "data:audio" in str(file_url) or "data:video" in str(file_url):
|
||||
# 处理 Base64 文件 (需要解码并作为文件上传)
|
||||
import base64
|
||||
import io
|
||||
b64_data = str(file_url)
|
||||
if b64_data.startswith("base64://"):
|
||||
b64_data = b64_data[9:]
|
||||
if b64_data.startswith("data:image") or b64_data.startswith("data:audio") or b64_data.startswith("data:video"):
|
||||
b64_data = b64_data.split(",", 1)[1]
|
||||
try:
|
||||
file_bytes = base64.b64decode(b64_data)
|
||||
filename = "file.png" if seg_type == "image" else ("file.mp4" if seg_type == "video" else "file.ogg")
|
||||
files.append(discord.File(fp=io.BytesIO(file_bytes), filename=filename))
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Base64 文件失败: {e}")
|
||||
else:
|
||||
# 假设是本地文件路径
|
||||
try:
|
||||
files.append(discord.File(file_url))
|
||||
except Exception as e:
|
||||
logger.error(f"无法读取本地文件 {file_url}: {e}")
|
||||
elif seg_type == "face":
|
||||
face_id = seg_data.get("id")
|
||||
content += f"[表情:{face_id}]"
|
||||
elif seg_type == "at":
|
||||
qq_id = seg_data.get("qq")
|
||||
if qq_id == "all":
|
||||
content += "@everyone "
|
||||
else:
|
||||
# 尝试将 QQ 号映射回 Discord ID (这里简单处理,直接拼接)
|
||||
content += f"<@{qq_id}> "
|
||||
elif seg_type == "reply":
|
||||
# 忽略回复段,或者你可以尝试映射 message_id
|
||||
pass
|
||||
elif seg_type == "reply":
|
||||
# 忽略回复段,或者你可以尝试映射 message_id
|
||||
pass
|
||||
|
||||
# 发送消息到 Discord
|
||||
try:
|
||||
# 发送消息到 Discord
|
||||
# 如果内容为空但有文件,Discord 允许发送
|
||||
if content or files:
|
||||
await channel.send(content=content, files=files if files else None)
|
||||
@@ -556,3 +558,5 @@ class DiscordToOneBotConverter:
|
||||
logger.warning("尝试发送空消息到 Discord,已拦截")
|
||||
except Exception as e:
|
||||
logger.error(f"发送 Discord 消息失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"异常堆栈: {traceback.format_exc()}")
|
||||
|
||||
Reference in New Issue
Block a user