Merge pull request #70 from Fairy-Oracle-Sanctuary/dev

feat(跨平台): 增强跨平台消息互通功能
This commit is contained in:
镀铬酸钾
2026-03-15 16:49:12 +08:00
committed by GitHub
3 changed files with 422 additions and 101 deletions

View File

@@ -32,22 +32,30 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
def __init__(self, token: str): def __init__(self, token: str):
if not DISCORD_AVAILABLE: if not DISCORD_AVAILABLE:
raise ImportError("discord.py 未安装,请运行 `pip install discord.py`") raise ImportError("discord.py 未安装,请运行 `pip install discord.py`")
# 必须声明 Intents否则无法读取消息内容
intents = discord.Intents.default()
intents.message_content = True
# 检查是否配置了代理 self.logger = ModuleLogger("DiscordAdapter")
self.token = token
self.send_channel = None
self.proxy = None self.proxy = None
self.proxy_type = "http" self.proxy_type = "http"
if global_config.discord.proxy: if global_config.discord.proxy:
self.proxy = global_config.discord.proxy self.proxy = global_config.discord.proxy
self.proxy_type = global_config.discord.proxy_type or "http" self.proxy_type = global_config.discord.proxy_type or "http"
proxy_url = self.proxy
if self.proxy_type.lower() in ["socks5", "socks4"]:
if not proxy_url.startswith(("socks5://", "socks4://")):
proxy_url = f"{self.proxy_type.lower()}://{proxy_url.split('://')[-1]}"
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
self.logger.info(f"[DiscordAdapter] 代理已设置: {proxy_url} (类型: {self.proxy_type})")
intents = discord.Intents.default()
intents.message_content = True
super().__init__(intents=intents) super().__init__(intents=intents)
self.token = token
self.logger = ModuleLogger("DiscordAdapter")
self.send_channel = None
async def on_ready(self): async def on_ready(self):
"""当 Bot 成功连接到 Discord 时触发""" """当 Bot 成功连接到 Discord 时触发"""
@@ -110,6 +118,7 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
channel_id = data.get("channel_id") channel_id = data.get("channel_id")
content = data.get("content", "") content = data.get("content", "")
attachments = data.get("attachments", []) attachments = data.get("attachments", [])
embed_data = data.get("embed")
if channel_id is None: if channel_id is None:
self.logger.error("[DiscordAdapter] 缺少 channel_id") self.logger.error("[DiscordAdapter] 缺少 channel_id")
@@ -122,9 +131,38 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
self.logger.info(f"[DiscordAdapter] 正在发送消息到频道 {channel_id}") self.logger.info(f"[DiscordAdapter] 正在发送消息到频道 {channel_id}")
# 发送内容和附件(合并为一条消息) files = []
if content or attachments: if attachments:
await channel.send(content=content, files=[discord.File(fp=io.BytesIO(requests.get(attachment_url).content), filename=os.path.basename(attachment_url)) for attachment_url in attachments if attachment_url.startswith('http')] if attachments else None) proxies = None
if self.proxy:
proxies = {
"http": self.proxy,
"https": self.proxy
}
for attachment in attachments:
if isinstance(attachment, dict):
attachment_url = attachment.get("url", "")
filename = attachment.get("filename", "")
else:
attachment_url = str(attachment)
filename = ""
if attachment_url.startswith('http'):
try:
response = requests.get(attachment_url, proxies=proxies, timeout=30)
if not filename:
filename = os.path.basename(attachment_url.split('?')[0]) or "attachment"
files.append(discord.File(fp=io.BytesIO(response.content), filename=filename))
except Exception as e:
self.logger.error(f"[DiscordAdapter] 下载附件失败: {attachment_url}, 错误: {e}")
embed = None
if embed_data:
embed = discord.Embed.from_dict(embed_data)
if content or files or embed:
await channel.send(content=content, files=files if files else None, embed=embed)
self.logger.success(f"[DiscordAdapter] 消息已发送到频道 {channel_id}") self.logger.success(f"[DiscordAdapter] 消息已发送到频道 {channel_id}")
@@ -139,35 +177,6 @@ class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
try: try:
self.logger.info("正在连接 Discord...") self.logger.info("正在连接 Discord...")
await self.start(self.token)
# 如果配置了代理,使用自定义的 ClientSession
if self.proxy:
import aiohttp
proxy_url = self.proxy
self.logger.info(f"[DiscordAdapter] 使用代理: {proxy_url} (类型: {self.proxy_type})")
connector = aiohttp.TCPConnector()
session = aiohttp.ClientSession(connector=connector)
# discord.py 2.0+ 使用 discord.Client 的 connector 参数
# 但 discord.Client 不直接支持自定义 connector
# 需要使用 discord.AutoShardedClient 或修改内部实现
# 这里我们使用 discord.Client 的 __init__ 传递 connector
# 但 discord.Client 的 __init__ 不支持 connector 参数
# 所以我们需要使用 discord.Client 的 _create_http_client 方法
# 简单方案:使用环境变量设置代理
import os
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
self.logger.info("[DiscordAdapter] 代理已设置,正在连接 Discord...")
await self.start(self.token)
# 清理环境变量
os.environ.pop("HTTP_PROXY", None)
os.environ.pop("HTTPS_PROXY", None)
else:
await self.start(self.token)
except Exception as e: except Exception as e:
self.logger.error(f"Discord 连接失败: {e}") self.logger.error(f"Discord 连接失败: {e}")

View File

@@ -103,7 +103,7 @@ base_url = "http://101.36.126.55:3003" # 外部访问的 URL
[discord] [discord]
enabled = true enabled = true
token = "MTQ4MjQzODA1NzExNzYxODI4Nw.G9R6uR.ddxHn3pmUf7SyrrOBg_-_lc7Y62lsCitPxpdGM" token = "MTQ4MjQzODA1NzExNzYxODI4Nw.G9R6uR.ddxHn3pmUf7SyrrOBg_-_lc7Y62lsCitPxpdGM"
proxy = "http://127.0.0.1:7890" proxy = "http://127.0.0.1:7897"
proxy_type = "http" proxy_type = "http"
# 跨平台消息互通配置 # 跨平台消息互通配置
@@ -126,7 +126,7 @@ name = "Paw"
# 日志配置 # 日志配置
[logging] [logging]
# 控制台日志级别DEBUG, INFO, SUCCESS, WARNING, ERROR # 控制台日志级别DEBUG, INFO, SUCCESS, WARNING, ERROR
console_level = "INFO" console_level = "DEBUG"
# 文件日志级别DEBUG, INFO, SUCCESS, WARNING, ERROR # 文件日志级别DEBUG, INFO, SUCCESS, WARNING, ERROR
file_level = "DEBUG" file_level = "DEBUG"
# 全局日志级别DEBUG, INFO, SUCCESS, WARNING, ERROR # 全局日志级别DEBUG, INFO, SUCCESS, WARNING, ERROR

View File

@@ -6,9 +6,12 @@
- 在消息中自动标注来源平台和子频道/群组 ID - 在消息中自动标注来源平台和子频道/群组 ID
- 支持 OneBot v11 协议和数据结构 - 支持 OneBot v11 协议和数据结构
- 支持图片、视频等媒体消息 - 支持图片、视频等媒体消息
- 支持合并转发消息
""" """
import asyncio import asyncio
import html
import json import json
import os
import re import re
import time import time
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
@@ -35,6 +38,236 @@ CROSS_PLATFORM_CHANNEL = "neobot_cross_platform"
ENABLE_CROSS_PLATFORM = True ENABLE_CROSS_PLATFORM = True
async def parse_forward_nodes(nodes: List[Dict[str, Any]]) -> tuple[str, List[dict]]:
"""
解析 OneBot 合并转发消息节点
Args:
nodes: 合并转发消息节点列表
Returns:
格式化后的消息内容和附件列表
"""
content_parts = []
attachments = []
for node in nodes:
if not isinstance(node, dict):
continue
node_data = node.get("data", {})
node_content = node_data.get("content", "")
# 获取发送者信息
sender_name = node_data.get("name", node_data.get("uin", "Unknown"))
# 解析节点内容
if isinstance(node_content, str):
# 检查是否是 [object Object] 格式OneBot 协议的特殊格式)
if "[object Object]" in node_content:
# OneBot 协议中,合并转发消息的 content 可能是 [object Object],[object Object]
# 实际的消息内容在 nodes 中,直接使用节点作为消息内容
content = f"[合并转发消息: {sender_name}]"
content_parts.append(f"**{sender_name}**:\n{content}")
elif '[CQ:' in node_content:
# CQ 码字符串格式
content = parse_cq_code(node_content, attachments)
content_parts.append(f"**{sender_name}**:\n{content}")
else:
content = node_content
content_parts.append(f"**{sender_name}**:\n{content}")
elif isinstance(node_content, list):
# MessageSegment 列表格式
content = parse_message_segments(node_content, attachments)
content_parts.append(f"**{sender_name}**:\n{content}")
# 组合完整消息
full_content = "\n\n".join(content_parts) if content_parts else ""
return full_content, attachments
def parse_cq_code(cq_code: str, attachments: List[dict]) -> str:
"""
解析 CQ 码字符串
Args:
cq_code: CQ 码字符串
attachments: 附件列表(用于添加图片/视频 URL
Returns:
解析后的文本内容
"""
import re
# 匹配 CQ 码
cq_pattern = r'\[CQ:([^,]+)(?:,([^\]]+))?\]'
matches = list(re.finditer(cq_pattern, cq_code))
if not matches:
return cq_code
result = []
last_end = 0
for match in matches:
if match.start() > last_end:
result.append(cq_code[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 == "text":
result.append(params.get("text", ""))
elif cq_type == "image":
file_url = params.get("url") or params.get("file")
if file_url:
file_name = params.get("file", "")
if not file_name:
file_name = os.path.basename(str(file_url).split('?')[0]) or "image"
attachments.append({"url": str(file_url), "filename": file_name})
result.append(f"\n[图片: {file_name}]\n")
elif cq_type == "video":
file_url = params.get("url") or params.get("file")
if file_url:
file_name = params.get("file", "")
if not file_name:
file_name = os.path.basename(str(file_url).split('?')[0]) or "video"
attachments.append({"url": str(file_url), "filename": file_name})
result.append(f"\n[视频: {file_name}]\n")
elif cq_type == "at":
qq_id = params.get("qq")
if qq_id == "all":
result.append("@所有人 ")
else:
result.append(f"@{qq_id} ")
elif cq_type == "face":
face_id = params.get("id", "")
result.append(f"[表情:{face_id}] ")
elif cq_type == "reply":
reply_id = params.get("id", "")
result.append(f"[回复:{reply_id}] ")
elif cq_type == "file":
file_url = params.get("file", "")
if file_url:
file_name = os.path.basename(str(file_url).split('?')[0]) or "file"
attachments.append({"url": str(file_url), "filename": file_name})
result.append(f"\n[文件: {file_name}]\n")
last_end = match.end()
if last_end < len(cq_code):
result.append(cq_code[last_end:])
return "".join(result)
def parse_message_segments(segments: List[Any], attachments: List[dict]) -> str:
"""
解析 MessageSegment 列表
Args:
segments: MessageSegment 列表
attachments: 附件列表(用于添加图片/视频 URL
Returns:
解析后的文本内容
"""
result = []
for seg in segments:
if isinstance(seg, str):
result.append(seg)
elif isinstance(seg, MessageSegment):
seg_type = seg.type
seg_data = seg.data
if seg_type == "text":
result.append(seg_data.get("text", ""))
elif seg_type == "image":
file_url = seg_data.get("url") or seg_data.get("file")
if file_url:
file_name = seg_data.get("file", "")
if not file_name:
file_name = os.path.basename(str(file_url).split('?')[0]) or "image"
attachments.append({"url": str(file_url), "filename": file_name})
result.append(f"\n[图片: {file_name}]\n")
elif seg_type == "video":
file_url = seg_data.get("url") or seg_data.get("file")
if file_url:
file_name = seg_data.get("file", "")
if not file_name:
file_name = os.path.basename(str(file_url).split('?')[0]) or "video"
attachments.append({"url": str(file_url), "filename": file_name})
result.append(f"\n[视频: {file_name}]\n")
elif seg_type == "at":
qq_id = seg_data.get("qq")
if qq_id == "all":
result.append("@所有人 ")
else:
result.append(f"@{qq_id} ")
elif seg_type == "face":
face_id = seg_data.get("id", "")
result.append(f"[表情:{face_id}] ")
elif seg_type == "reply":
reply_id = seg_data.get("id", "")
result.append(f"[回复:{reply_id}] ")
elif seg_type == "file":
file_url = seg_data.get("file", "")
if file_url:
file_name = os.path.basename(str(file_url).split('?')[0]) or "file"
attachments.append({"url": str(file_url), "filename": file_name})
result.append(f"\n[文件: {file_name}]\n")
elif seg_type == "json":
# 尝试解析 JSON 数据
json_data = seg_data.get("data", "")
try:
parsed = json.loads(json_data)
if isinstance(parsed, dict):
result.append(f"\n[JSON数据: {json_data[:100]}...]\n")
except:
result.append(f"\n[JSON数据]\n")
elif seg_type == "xml":
result.append(f"\n[XML数据]\n")
elif isinstance(seg, dict):
seg_type = seg.get("type")
seg_data = seg.get("data", {})
if seg_type == "text":
result.append(seg_data.get("text", ""))
elif seg_type == "image":
file_url = seg_data.get("url") or seg_data.get("file")
if file_url:
file_name = seg_data.get("file", "")
if not file_name:
file_name = os.path.basename(str(file_url).split('?')[0]) or "image"
attachments.append({"url": str(file_url), "filename": file_name})
result.append(f"\n[图片: {file_name}]\n")
elif seg_type == "video":
file_url = seg_data.get("url") or seg_data.get("file")
if file_url:
file_name = seg_data.get("file", "")
if not file_name:
file_name = os.path.basename(str(file_url).split('?')[0]) or "video"
attachments.append({"url": str(file_url), "filename": file_name})
result.append(f"\n[视频: {file_name}]\n")
elif seg_type == "at":
qq_id = seg_data.get("qq")
if qq_id == "all":
result.append("@所有人 ")
else:
result.append(f"@{qq_id} ")
return "".join(result)
def get_platform_info(platform: str, identifier: Any) -> str: def get_platform_info(platform: str, identifier: Any) -> str:
""" """
获取平台信息字符串,用于在消息中标注来源 获取平台信息字符串,用于在消息中标注来源
@@ -55,7 +288,7 @@ def get_platform_info(platform: str, identifier: Any) -> str:
return f"[Discord]" return f"[Discord]"
elif platform == "qq": elif platform == "qq":
group_id = int(identifier) group_id = int(identifier)
return f"[QQ {group_id}]" return f"[PAW qq]"
return "" return ""
@@ -64,7 +297,7 @@ async def format_discord_to_qq_content(
discord_discriminator: str, discord_discriminator: str,
content: str, content: str,
channel_id: int, channel_id: int,
attachments: List[str] = None attachments: List[dict] = None
) -> tuple[str, List[str]]: ) -> tuple[str, List[str]]:
""" """
将 Discord 消息格式化为 QQ 消息格式 将 Discord 消息格式化为 QQ 消息格式
@@ -93,7 +326,18 @@ async def format_discord_to_qq_content(
else: else:
full_message = message_header full_message = message_header
return full_message, attachments or [] # 提取图片 URL
image_list = []
if attachments:
for att in attachments:
if isinstance(att, dict):
url = att.get("url", "")
if url.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
image_list.append(url)
else:
image_list.append(str(att))
return full_message, image_list
async def format_qq_to_discord_content( async def format_qq_to_discord_content(
@@ -102,10 +346,10 @@ async def format_qq_to_discord_content(
group_name: str, group_name: str,
group_id: int, group_id: int,
content: str, content: str,
attachments: List[str] = None attachments: List[dict] = None
) -> tuple[str, List[str]]: ) -> tuple[str, List[dict], dict]:
""" """
将 QQ 消息格式化为 Discord 消息格式 将 QQ 消息格式化为 Discord 消息格式Embed 卡片)
Args: Args:
qq_nickname: QQ 昵称 qq_nickname: QQ 昵称
@@ -116,26 +360,53 @@ async def format_qq_to_discord_content(
attachments: 附件列表 attachments: 附件列表
Returns: Returns:
格式化后的消息内容和图片列表 格式化后的消息内容、附件列表和 Embed 字典
""" """
platform_info = get_platform_info("qq", group_id) platform_info = get_platform_info("qq", group_id)
# 构建消息头(简化版,只显示名字) # 构建 Embed 卡片
message_header = f"{platform_info} {qq_nickname}:" embed = {
"type": "rich",
"color": 0x5865F2, # Discord 蓝色
"author": {
"name": f"{platform_info} {qq_nickname}",
"icon_url": f"https://q1.qlogo.cn/g?b=qq&nk={qq_user_id}&s=640"
},
"description": content if content else "",
"timestamp": None,
"footer": {
"text": f"来自 QQPAW"
}
}
# 构建消息体 # 如果有附件,添加到 description
message_body = content if content else "" if attachments:
image_urls = []
other_urls = []
for att in attachments:
if att.get("url", "").lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
image_urls.append(att.get("url"))
else:
other_urls.append(att.get("url"))
if image_urls:
embed["description"] = f"{content}\n\n{chr(10).join(image_urls[:3])}" if content else chr(10).join(image_urls[:3])
if len(image_urls) > 3:
embed["description"] += f"\n...还有 {len(image_urls) - 3} 张图片"
# 附加文件列表
if other_urls:
file_list = "\n".join([f"📄 {os.path.basename(u.split('?')[0])}" for u in other_urls[:5]])
embed["description"] += f"\n\n**附加文件:**\n{file_list}"
if len(other_urls) > 5:
embed["description"] += f"\n...还有 {len(other_urls) - 5} 个文件"
# 组合完整消息(移除分隔符) # 对于合并转发消息content 为空,只发送 embed
if message_body: # 对于普通消息content 也为空,只发送 embed
full_message = f"{message_header} {message_body}" return "", attachments or [], embed
else:
full_message = message_header
return full_message, attachments or []
async def send_to_discord(channel_id: int, content: str, attachments: List[str] = None): async def send_to_discord(channel_id: int, content: str, attachments: List[dict] = None, embed: dict = None):
""" """
发送消息到 Discord 频道 发送消息到 Discord 频道
@@ -145,14 +416,16 @@ async def send_to_discord(channel_id: int, content: str, attachments: List[str]
Args: Args:
channel_id: Discord 频道 ID channel_id: Discord 频道 ID
content: 消息内容 content: 消息内容
attachments: 附件 URL 列表 attachments: 附件列表,每个元素为 {"url": str, "filename": str}
embed: Discord Embed 字典
""" """
try: try:
publish_data = { publish_data = {
"type": "send_message", "type": "send_message",
"channel_id": channel_id, "channel_id": channel_id,
"content": content, "content": content,
"attachments": attachments or [] "attachments": attachments or [],
"embed": embed
} }
await redis_manager.redis.publish("neobot_discord_send", json.dumps(publish_data)) await redis_manager.redis.publish("neobot_discord_send", json.dumps(publish_data))
logger.info(f"[CrossPlatform] 消息已发布到 Redis 供 Discord 适配器发送: {channel_id}") logger.info(f"[CrossPlatform] 消息已发布到 Redis 供 Discord 适配器发送: {channel_id}")
@@ -161,14 +434,14 @@ async def send_to_discord(channel_id: int, content: str, attachments: List[str]
logger.error(f"[CrossPlatform] 发送消息到 Discord 失败: {e}") logger.error(f"[CrossPlatform] 发送消息到 Discord 失败: {e}")
async def send_to_qq(group_id: int, content: str, attachments: List[str] = None): async def send_to_qq(group_id: int, content: str, attachments: List[dict] = None):
""" """
发送消息到 QQ 群 发送消息到 QQ 群
Args: Args:
group_id: QQ 群 ID group_id: QQ 群 ID
content: 消息内容 content: 消息内容
attachments: 附件 URL 列表 attachments: 附件列表,每个元素为 {"url": str, "filename": str}
""" """
try: try:
from core.managers.bot_manager import bot_manager from core.managers.bot_manager import bot_manager
@@ -196,7 +469,11 @@ async def send_to_qq(group_id: int, content: str, attachments: List[str] = None)
if content: if content:
full_message.append(MessageSegment.text(content)) full_message.append(MessageSegment.text(content))
for attachment in attachments: for attachment in attachments:
full_message.append(MessageSegment.image(attachment, cache=True, proxy=True, timeout=30)) if isinstance(attachment, dict):
attachment_url = attachment.get("url", "")
else:
attachment_url = str(attachment)
full_message.append(MessageSegment.image(attachment_url, cache=True, proxy=True, timeout=30))
logger.debug(f"[CrossPlatform] 准备发送消息到 QQ 群 {group_id}: {full_message}") logger.debug(f"[CrossPlatform] 准备发送消息到 QQ 群 {group_id}: {full_message}")
# 一次性发送 # 一次性发送
@@ -219,7 +496,7 @@ async def forward_discord_to_qq(
discord_discriminator: str, discord_discriminator: str,
content: str, content: str,
channel_id: int, channel_id: int,
attachments: List[str] = None attachments: List[dict] = None
): ):
""" """
将 Discord 消息转发到所有映射的 QQ 群 将 Discord 消息转发到所有映射的 QQ 群
@@ -259,7 +536,7 @@ async def forward_qq_to_discord(
group_name: str, group_name: str,
group_id: int, group_id: int,
content: str, content: str,
attachments: List[str] = None attachments: List[dict] = None
): ):
""" """
将 QQ 消息转发到所有映射的 Discord 频道 将 QQ 消息转发到所有映射的 Discord 频道
@@ -283,7 +560,7 @@ async def forward_qq_to_discord(
return return
# 格式化消息 # 格式化消息
formatted_content, image_list = await format_qq_to_discord_content( formatted_content, image_list, embed = await format_qq_to_discord_content(
qq_nickname, qq_nickname,
qq_user_id, qq_user_id,
group_name, group_name,
@@ -294,7 +571,7 @@ async def forward_qq_to_discord(
# 发送到所有映射的 Discord 频道 # 发送到所有映射的 Discord 频道
for channel_id in target_channels: for channel_id in target_channels:
await send_to_discord(channel_id, formatted_content, image_list) await send_to_discord(channel_id, formatted_content, image_list, embed)
logger.success(f"[CrossPlatform] QQ 群 {group_id} -> Discord 频道 {target_channels}") logger.success(f"[CrossPlatform] QQ 群 {group_id} -> Discord 频道 {target_channels}")
@@ -325,7 +602,8 @@ async def handle_discord_message(
discriminator: str, discriminator: str,
content: str, content: str,
channel_id: int, channel_id: int,
attachments: List[str] = None attachments: List[dict] = None,
embed: dict = None
): ):
""" """
处理 Discord 消息并转发 处理 Discord 消息并转发
@@ -336,6 +614,7 @@ async def handle_discord_message(
content: 消息内容 content: 消息内容
channel_id: Discord 频道 ID channel_id: Discord 频道 ID
attachments: 附件列表 attachments: 附件列表
embed: Discord Embed 字典
""" """
if not ENABLE_CROSS_PLATFORM: if not ENABLE_CROSS_PLATFORM:
return return
@@ -352,7 +631,7 @@ async def handle_qq_message(
group_name: str, group_name: str,
group_id: int, group_id: int,
content: str, content: str,
attachments: List[str] = None attachments: List[dict] = None
): ):
""" """
处理 QQ 消息并转发 处理 QQ 消息并转发
@@ -398,26 +677,45 @@ async def handle_qq_group_message(event: GroupMessageEvent):
attachments = [] attachments = []
if isinstance(event.message, list): if isinstance(event.message, list):
for segment in event.message: # 检查是否是合并转发消息
if isinstance(segment, MessageSegment): has_forward_node = any(isinstance(seg, MessageSegment) and seg.type == "node" for seg in event.message)
if segment.type == "text":
content += segment.data.get("text", "") if has_forward_node:
elif segment.type == "image": # 解析合并转发消息
file_url = segment.data.get("url") or segment.data.get("file") forward_nodes = [seg for seg in event.message if isinstance(seg, MessageSegment) and seg.type == "node"]
if file_url: # 将 MessageSegment 转换为字典格式
attachments.append(str(file_url)) forward_nodes_dict = [{"type": seg.type, "data": seg.data} for seg in forward_nodes]
elif segment.type == "video": content, attachments = await parse_forward_nodes(forward_nodes_dict)
file_url = segment.data.get("url") or segment.data.get("file") else:
if file_url: # 普通消息
attachments.append(str(file_url)) for segment in event.message:
elif segment.type == "at": if isinstance(segment, MessageSegment):
qq_id = segment.data.get("qq") if segment.type == "text":
if qq_id and qq_id != "all": content += segment.data.get("text", "")
content += f"@{qq_id} " elif segment.type == "image":
elif qq_id == "all": file_url = segment.data.get("url") or segment.data.get("file")
content += "@所有人 " file_name = segment.data.get("file", "")
elif isinstance(segment, str): if file_url:
content += segment 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({"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("file", "")
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({"url": file_url, "filename": file_name})
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): elif isinstance(event.message, str):
content = event.message content = event.message
@@ -473,8 +771,12 @@ async def handle_discord_message_event(event: Any):
line = line.strip() line = line.strip()
if re.match(url_pattern, line): if re.match(url_pattern, line):
# 这是附件 URL # 这是附件 URL
if line not in attachments: attachment_url = line
attachments.append(line) # 尝试从 URL 提取文件名
filename = os.path.basename(attachment_url.split('?')[0]) or "attachment"
attachment_item = {"url": attachment_url, "filename": filename}
if attachment_item not in attachments:
attachments.append(attachment_item)
else: else:
# 这是普通文本内容 # 这是普通文本内容
if line: if line:
@@ -490,12 +792,20 @@ async def handle_discord_message_event(event: Any):
pass # 已经在 raw_message 中 pass # 已经在 raw_message 中
elif segment.type == "image": elif segment.type == "image":
file_url = segment.data.get("url") or segment.data.get("file") file_url = segment.data.get("url") or segment.data.get("file")
if file_url and str(file_url) not in attachments: file_name = segment.data.get("file", "")
attachments.append(str(file_url)) if file_url:
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "image"
attachment_item = {"url": str(file_url), "filename": file_name}
if attachment_item not in attachments:
attachments.append(attachment_item)
elif segment.type == "video": elif segment.type == "video":
file_url = segment.data.get("url") or segment.data.get("file") file_url = segment.data.get("url") or segment.data.get("file")
if file_url and str(file_url) not in attachments: file_name = segment.data.get("file", "")
attachments.append(str(file_url)) if file_url:
file_name = file_name or os.path.basename(str(file_url).split('?')[0]) or "video"
attachment_item = {"url": str(file_url), "filename": file_name}
if attachment_item not in attachments:
attachments.append(attachment_item)
# 获取用户信息 # 获取用户信息
discord_username = getattr(event, 'discord_username', 'Unknown') discord_username = getattr(event, 'discord_username', 'Unknown')
@@ -507,7 +817,8 @@ async def handle_discord_message_event(event: Any):
discriminator=discord_discriminator, discriminator=discord_discriminator,
content=content, content=content,
channel_id=discord_channel_id, channel_id=discord_channel_id,
attachments=attachments attachments=attachments,
embed=None
) )
@@ -551,7 +862,8 @@ async def cross_platform_subscription_loop():
group_name=message_data.get("group_name", ""), group_name=message_data.get("group_name", ""),
group_id=message_data.get("group_id", 0), group_id=message_data.get("group_id", 0),
content=message_data.get("content", ""), content=message_data.get("content", ""),
attachments=message_data.get("attachments", []) attachments=message_data.get("attachments", []),
embed=message_data.get("embed")
) )
except json.JSONDecodeError as e: except json.JSONDecodeError as e: