Dev (#78)
* 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检查条件 删除不再使用的示例插件文件 * feat: 改进配置加载逻辑并更新项目配置 当配置文件不存在时自动生成示例配置 添加pyproject.toml作为项目构建配置 更新.gitignore忽略更多文件类型 删除不再使用的反向WebSocket示例文件 * docs: 更新架构文档和项目结构说明 添加反向WebSocket连接模式说明 补充核心管理器文档 更新项目结构文件 在文档首页添加特色功能说明 * fix(discord): 修复WebSocket连接检查并添加错误日志 refactor(config): 更新配置文件的网络和认证信息 feat(cross-platform): 为跨平台消息处理添加异常捕获和日志
This commit is contained in:
@@ -51,195 +51,205 @@ async def handle_qq_message(
|
||||
@matcher.on_message()
|
||||
async def handle_qq_group_message(event: GroupMessageEvent):
|
||||
"""处理 QQ 群消息,转发到 Discord"""
|
||||
if not config.ENABLE_CROSS_PLATFORM:
|
||||
return
|
||||
|
||||
group_id = event.group_id
|
||||
mapped_channel = None
|
||||
for discord_channel_id, info in config.CROSS_PLATFORM_MAP.items():
|
||||
if info["qq_group_id"] == group_id:
|
||||
mapped_channel = discord_channel_id
|
||||
break
|
||||
|
||||
if mapped_channel is None:
|
||||
return
|
||||
|
||||
content = ""
|
||||
attachments = []
|
||||
|
||||
if isinstance(event.message, list):
|
||||
has_forward_node = any(isinstance(seg, MessageSegment) and seg.type == "node" for seg in event.message)
|
||||
|
||||
if has_forward_node:
|
||||
forward_nodes = [seg for seg in event.message if isinstance(seg, MessageSegment) and seg.type == "node"]
|
||||
forward_nodes_dict = [{"type": seg.type, "data": seg.data} for seg in forward_nodes]
|
||||
content, attachments = await parse_forward_nodes(forward_nodes_dict)
|
||||
else:
|
||||
for segment in event.message:
|
||||
if isinstance(segment, MessageSegment):
|
||||
if segment.type == "text":
|
||||
content += segment.data.get("text", "")
|
||||
elif segment.type == "image":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_url = html.unescape(str(file_url))
|
||||
if not file_name:
|
||||
file_name = os.path.basename(file_url.split('?')[0]) or f"image_{len(attachments)}.jpg"
|
||||
attachments.append({"type": "image", "url": file_url, "filename": file_name})
|
||||
elif segment.type == "video":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_url = html.unescape(str(file_url))
|
||||
if not file_name:
|
||||
file_name = os.path.basename(file_url.split('?')[0]) or f"video_{len(attachments)}.mp4"
|
||||
attachments.append({"type": "video", "url": file_url, "filename": file_name})
|
||||
elif segment.type == "record":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_url = html.unescape(str(file_url))
|
||||
if not file_name:
|
||||
file_name = os.path.basename(file_url.split('?')[0]) or f"record_{len(attachments)}.amr"
|
||||
attachments.append({"type": "record", "url": file_url, "filename": file_name})
|
||||
content += f"\n[语音: {file_name}]\n"
|
||||
elif segment.type == "file":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_url = html.unescape(str(file_url))
|
||||
if not file_name:
|
||||
file_name = os.path.basename(file_url.split('?')[0]) or f"file_{len(attachments)}"
|
||||
attachments.append({"type": "file", "url": file_url, "filename": file_name})
|
||||
content += f"\n[文件: {file_name}]\n"
|
||||
logger.debug(f"[CrossPlatform] QQ 消息识别到文件: {file_name}, URL: {file_url}")
|
||||
elif segment.type == "at":
|
||||
qq_id = segment.data.get("qq")
|
||||
if qq_id and qq_id != "all":
|
||||
content += f"@{qq_id} "
|
||||
elif qq_id == "all":
|
||||
content += "@所有人 "
|
||||
elif isinstance(segment, str):
|
||||
content += segment
|
||||
elif isinstance(event.message, str):
|
||||
content = event.message
|
||||
|
||||
import re
|
||||
local_file_pattern = r'(http://[\w\.-]+:\d+/download\?id=file_[a-zA-Z0-9_]+)'
|
||||
matches = re.finditer(local_file_pattern, content)
|
||||
for match in matches:
|
||||
file_url = match.group(1)
|
||||
file_name = f"video_{len(attachments)}.mp4"
|
||||
attachments.append({"type": "video", "url": file_url, "filename": file_name})
|
||||
|
||||
content = content.strip()
|
||||
|
||||
group_name = ""
|
||||
try:
|
||||
group_info = await event.bot.get_group_info(event.group_id)
|
||||
group_name = group_info.get("group_name", "")
|
||||
except Exception:
|
||||
group_name = f"群{group_id}"
|
||||
|
||||
await handle_qq_message(
|
||||
nickname=event.sender.nickname or event.sender.card or str(event.user_id),
|
||||
user_id=event.user_id,
|
||||
group_name=group_name,
|
||||
group_id=group_id,
|
||||
content=content,
|
||||
attachments=attachments
|
||||
)
|
||||
if not config.ENABLE_CROSS_PLATFORM:
|
||||
return
|
||||
|
||||
group_id = event.group_id
|
||||
mapped_channel = None
|
||||
for discord_channel_id, info in config.CROSS_PLATFORM_MAP.items():
|
||||
if info["qq_group_id"] == group_id:
|
||||
mapped_channel = discord_channel_id
|
||||
break
|
||||
|
||||
if mapped_channel is None:
|
||||
return
|
||||
|
||||
content = ""
|
||||
attachments = []
|
||||
|
||||
if isinstance(event.message, list):
|
||||
has_forward_node = any(isinstance(seg, MessageSegment) and seg.type == "node" for seg in event.message)
|
||||
|
||||
if has_forward_node:
|
||||
forward_nodes = [seg for seg in event.message if isinstance(seg, MessageSegment) and seg.type == "node"]
|
||||
forward_nodes_dict = [{"type": seg.type, "data": seg.data} for seg in forward_nodes]
|
||||
content, attachments = await parse_forward_nodes(forward_nodes_dict)
|
||||
else:
|
||||
for segment in event.message:
|
||||
if isinstance(segment, MessageSegment):
|
||||
if segment.type == "text":
|
||||
content += segment.data.get("text", "")
|
||||
elif segment.type == "image":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_url = html.unescape(str(file_url))
|
||||
if not file_name:
|
||||
file_name = os.path.basename(file_url.split('?')[0]) or f"image_{len(attachments)}.jpg"
|
||||
attachments.append({"type": "image", "url": file_url, "filename": file_name})
|
||||
elif segment.type == "video":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_url = html.unescape(str(file_url))
|
||||
if not file_name:
|
||||
file_name = os.path.basename(file_url.split('?')[0]) or f"video_{len(attachments)}.mp4"
|
||||
attachments.append({"type": "video", "url": file_url, "filename": file_name})
|
||||
elif segment.type == "record":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_url = html.unescape(str(file_url))
|
||||
if not file_name:
|
||||
file_name = os.path.basename(file_url.split('?')[0]) or f"record_{len(attachments)}.amr"
|
||||
attachments.append({"type": "record", "url": file_url, "filename": file_name})
|
||||
content += f"\n[语音: {file_name}]\n"
|
||||
elif segment.type == "file":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_url = html.unescape(str(file_url))
|
||||
if not file_name:
|
||||
file_name = os.path.basename(file_url.split('?')[0]) or f"file_{len(attachments)}"
|
||||
attachments.append({"type": "file", "url": file_url, "filename": file_name})
|
||||
content += f"\n[文件: {file_name}]\n"
|
||||
logger.debug(f"[CrossPlatform] QQ 消息识别到文件: {file_name}, URL: {file_url}")
|
||||
elif segment.type == "at":
|
||||
qq_id = segment.data.get("qq")
|
||||
if qq_id and qq_id != "all":
|
||||
content += f"@{qq_id} "
|
||||
elif qq_id == "all":
|
||||
content += "@所有人 "
|
||||
elif isinstance(segment, str):
|
||||
content += segment
|
||||
elif isinstance(event.message, str):
|
||||
content = event.message
|
||||
|
||||
import re
|
||||
local_file_pattern = r'(http://[\w\.-]+:\d+/download\?id=file_[a-zA-Z0-9_]+)'
|
||||
matches = re.finditer(local_file_pattern, content)
|
||||
for match in matches:
|
||||
file_url = match.group(1)
|
||||
file_name = f"video_{len(attachments)}.mp4"
|
||||
attachments.append({"type": "video", "url": file_url, "filename": file_name})
|
||||
|
||||
content = content.strip()
|
||||
|
||||
group_name = ""
|
||||
try:
|
||||
group_info = await event.bot.get_group_info(event.group_id)
|
||||
group_name = group_info.get("group_name", "")
|
||||
except Exception:
|
||||
group_name = f"群{group_id}"
|
||||
|
||||
await handle_qq_message(
|
||||
nickname=event.sender.nickname or event.sender.card or str(event.user_id),
|
||||
user_id=event.user_id,
|
||||
group_name=group_name,
|
||||
group_id=group_id,
|
||||
content=content,
|
||||
attachments=attachments
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[CrossPlatform] 处理 QQ 群消息失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"[CrossPlatform] 异常堆栈: {traceback.format_exc()}")
|
||||
|
||||
@matcher.on_message()
|
||||
async def handle_discord_message_event(event: Any):
|
||||
"""处理 Discord 消息事件(通过适配器注入)"""
|
||||
if not config.ENABLE_CROSS_PLATFORM:
|
||||
return
|
||||
try:
|
||||
if not config.ENABLE_CROSS_PLATFORM:
|
||||
return
|
||||
|
||||
logger.debug(f"[CrossPlatform] handle_discord_message_event 触发: {event}")
|
||||
if not hasattr(event, '_is_discord_message'):
|
||||
logger.debug(f"[CrossPlatform] 事件没有 _is_discord_message 属性,跳过")
|
||||
return
|
||||
|
||||
logger.debug(f"[CrossPlatform] 检测到 Discord 事件")
|
||||
discord_channel_id = getattr(event, 'discord_channel_id', None)
|
||||
if discord_channel_id is None:
|
||||
logger.debug(f"[CrossPlatform] discord_channel_id 为 None")
|
||||
return
|
||||
|
||||
content = ""
|
||||
attachments = []
|
||||
|
||||
logger.debug(f"[CrossPlatform] handle_discord_message_event 触发: {event}")
|
||||
if not hasattr(event, '_is_discord_message'):
|
||||
logger.debug(f"[CrossPlatform] 事件没有 _is_discord_message 属性,跳过")
|
||||
return
|
||||
logger.debug(f"[CrossPlatform] 开始处理 Discord 事件消息: channel_id={discord_channel_id}")
|
||||
|
||||
logger.debug(f"[CrossPlatform] 检测到 Discord 事件")
|
||||
discord_channel_id = getattr(event, 'discord_channel_id', None)
|
||||
if discord_channel_id is None:
|
||||
logger.debug(f"[CrossPlatform] discord_channel_id 为 None")
|
||||
return
|
||||
if hasattr(event, 'message') and isinstance(event.message, list):
|
||||
has_text_content = False
|
||||
for segment in event.message:
|
||||
if isinstance(segment, MessageSegment):
|
||||
if segment.type == "text":
|
||||
content += segment.data.get("text", "")
|
||||
has_text_content = True
|
||||
elif segment.type == "image":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "image"
|
||||
attachment_item = {"type": "image", "url": str(file_url), "filename": file_name}
|
||||
if attachment_item not in attachments:
|
||||
attachments.append(attachment_item)
|
||||
content += f"\n[图片: {file_name}]\n"
|
||||
elif segment.type == "video":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "video"
|
||||
attachment_item = {"type": "video", "url": str(file_url), "filename": file_name}
|
||||
if attachment_item not in attachments:
|
||||
attachments.append(attachment_item)
|
||||
content += f"\n[视频: {file_name}]\n"
|
||||
elif segment.type == "record":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "record"
|
||||
attachment_item = {"type": "record", "url": str(file_url), "filename": file_name}
|
||||
if attachment_item not in attachments:
|
||||
attachments.append(attachment_item)
|
||||
content += f"\n[语音: {file_name}]\n"
|
||||
elif segment.type == "file":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "file"
|
||||
attachment_item = {"type": "file", "url": str(file_url), "filename": file_name}
|
||||
if attachment_item not in attachments:
|
||||
attachments.append(attachment_item)
|
||||
content += f"\n[文件: {file_name}]\n"
|
||||
logger.debug(f"[CrossPlatform] Discord 消息识别到文件: {file_name}, URL: {file_url}")
|
||||
else:
|
||||
content = event.raw_message or ""
|
||||
|
||||
content = ""
|
||||
attachments = []
|
||||
|
||||
logger.debug(f"[CrossPlatform] 开始处理 Discord 事件消息: channel_id={discord_channel_id}")
|
||||
|
||||
if hasattr(event, 'message') and isinstance(event.message, list):
|
||||
has_text_content = False
|
||||
for segment in event.message:
|
||||
if isinstance(segment, MessageSegment):
|
||||
if segment.type == "text":
|
||||
content += segment.data.get("text", "")
|
||||
has_text_content = True
|
||||
elif segment.type == "image":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "image"
|
||||
attachment_item = {"type": "image", "url": str(file_url), "filename": file_name}
|
||||
if attachment_item not in attachments:
|
||||
attachments.append(attachment_item)
|
||||
content += f"\n[图片: {file_name}]\n"
|
||||
elif segment.type == "video":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "video"
|
||||
attachment_item = {"type": "video", "url": str(file_url), "filename": file_name}
|
||||
if attachment_item not in attachments:
|
||||
attachments.append(attachment_item)
|
||||
content += f"\n[视频: {file_name}]\n"
|
||||
elif segment.type == "record":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "record"
|
||||
attachment_item = {"type": "record", "url": str(file_url), "filename": file_name}
|
||||
if attachment_item not in attachments:
|
||||
attachments.append(attachment_item)
|
||||
content += f"\n[语音: {file_name}]\n"
|
||||
elif segment.type == "file":
|
||||
file_url = segment.data.get("url") or segment.data.get("file")
|
||||
file_name = segment.data.get("filename")
|
||||
if file_url:
|
||||
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "file"
|
||||
attachment_item = {"type": "file", "url": str(file_url), "filename": file_name}
|
||||
if attachment_item not in attachments:
|
||||
attachments.append(attachment_item)
|
||||
content += f"\n[文件: {file_name}]\n"
|
||||
logger.debug(f"[CrossPlatform] Discord 消息识别到文件: {file_name}, URL: {file_url}")
|
||||
else:
|
||||
content = event.raw_message or ""
|
||||
|
||||
content = content.strip()
|
||||
|
||||
# 如果 content 为空但有附件(如只有表情),使用 raw_message 作为 content
|
||||
if not content and attachments:
|
||||
content = event.raw_message or ""
|
||||
|
||||
logger.debug(f"[CrossPlatform] Discord 消息内容: '{content}', 附件数量: {len(attachments)}")
|
||||
|
||||
discord_username = getattr(event, 'discord_username', 'Unknown')
|
||||
discord_discriminator = getattr(event, 'discord_discriminator', '')
|
||||
|
||||
logger.debug(f"[CrossPlatform] 调用 handle_discord_message: username={discord_username}, channel_id={discord_channel_id}")
|
||||
await handle_discord_message(
|
||||
username=discord_username,
|
||||
discriminator=discord_discriminator,
|
||||
content=content,
|
||||
channel_id=discord_channel_id,
|
||||
attachments=attachments,
|
||||
embed=None
|
||||
)
|
||||
content = content.strip()
|
||||
|
||||
# 如果 content 为空但有附件(如只有表情),使用 raw_message 作为 content
|
||||
if not content and attachments:
|
||||
content = event.raw_message or ""
|
||||
|
||||
logger.debug(f"[CrossPlatform] Discord 消息内容: '{content}', 附件数量: {len(attachments)}")
|
||||
|
||||
discord_username = getattr(event, 'discord_username', 'Unknown')
|
||||
discord_discriminator = getattr(event, 'discord_discriminator', '')
|
||||
|
||||
logger.debug(f"[CrossPlatform] 调用 handle_discord_message: username={discord_username}, channel_id={discord_channel_id}")
|
||||
await handle_discord_message(
|
||||
username=discord_username,
|
||||
discriminator=discord_discriminator,
|
||||
content=content,
|
||||
channel_id=discord_channel_id,
|
||||
attachments=attachments,
|
||||
embed=None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[CrossPlatform] 处理 Discord 消息事件失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"[CrossPlatform] 异常堆栈: {traceback.format_exc()}")
|
||||
|
||||
@matcher.command("cross_config", "跨平台配置", permission=Permission.ADMIN)
|
||||
async def cross_config_command(event: MessageEvent):
|
||||
|
||||
Reference in New Issue
Block a user