diff --git a/app/modules/discord/__init__.py b/app/modules/discord/__init__.py index 82fc8664..15ed851a 100644 --- a/app/modules/discord/__init__.py +++ b/app/modules/discord/__init__.py @@ -139,9 +139,23 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]): 发送通知消息 :param message: 消息通知对象 """ - for conf in self.get_configs().values(): + # DEBUG: Log entry and configs + configs = self.get_configs() + logger.debug(f"[Discord] post_message 被调用,message.source={message.source}, " + f"message.userid={message.userid}, message.channel={message.channel}") + logger.debug(f"[Discord] 当前配置数量: {len(configs)}, 配置名称: {list(configs.keys())}") + logger.debug(f"[Discord] 当前实例数量: {len(self.get_instances())}, 实例名称: {list(self.get_instances().keys())}") + + if not configs: + logger.warning("[Discord] get_configs() 返回空,没有可用的 Discord 配置") + return + + for conf in configs.values(): + logger.debug(f"[Discord] 检查配置: name={conf.name}, type={conf.type}, enabled={conf.enabled}") if not self.check_message(message, conf.name): + logger.debug(f"[Discord] check_message 返回 False,跳过配置: {conf.name}") continue + logger.debug(f"[Discord] check_message 通过,准备发送到: {conf.name}") targets = message.targets userid = message.userid if not userid and targets is not None: @@ -150,13 +164,18 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]): logger.warn("用户没有指定 Discord 用户ID,消息无法发送") return client: Discord = self.get_instance(conf.name) + logger.debug(f"[Discord] get_instance('{conf.name}') 返回: {client is not None}") if client: - client.send_msg(title=message.title, text=message.text, + logger.debug(f"[Discord] 调用 client.send_msg, userid={userid}, title={message.title[:50] if message.title else None}...") + result = client.send_msg(title=message.title, text=message.text, image=message.image, userid=userid, link=message.link, buttons=message.buttons, original_message_id=message.original_message_id, original_chat_id=message.original_chat_id, mtype=message.mtype) + logger.debug(f"[Discord] send_msg 返回结果: {result}") + else: + logger.warning(f"[Discord] 未找到配置 '{conf.name}' 对应的 Discord 客户端实例") def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None: """ diff --git a/app/modules/discord/discord.py b/app/modules/discord/discord.py index 6108d2da..f1997c5c 100644 --- a/app/modules/discord/discord.py +++ b/app/modules/discord/discord.py @@ -2,6 +2,7 @@ import asyncio import re import threading from typing import Optional, List, Dict, Any, Tuple, Union +from urllib.parse import quote import discord from discord import app_commands @@ -33,6 +34,9 @@ class Discord: DISCORD_GUILD_ID: Optional[Union[str, int]] = None, DISCORD_CHANNEL_ID: Optional[Union[str, int]] = None, **kwargs): + logger.debug(f"[Discord] 初始化 Discord 实例: name={kwargs.get('name')}, " + f"GUILD_ID={DISCORD_GUILD_ID}, CHANNEL_ID={DISCORD_CHANNEL_ID}, " + f"TOKEN={'已配置' if DISCORD_BOT_TOKEN else '未配置'}") if not DISCORD_BOT_TOKEN: logger.error("Discord Bot Token 未配置!") return @@ -40,10 +44,14 @@ class Discord: self._token = DISCORD_BOT_TOKEN self._guild_id = self._to_int(DISCORD_GUILD_ID) self._channel_id = self._to_int(DISCORD_CHANNEL_ID) + logger.debug(f"[Discord] 解析后的 ID: _guild_id={self._guild_id}, _channel_id={self._channel_id}") base_ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message/" self._ds_url = f"{base_ds_url}?token={settings.API_TOKEN}" if kwargs.get("name"): - self._ds_url = f"{self._ds_url}&source={kwargs.get('name')}" + # URL encode the source name to handle special characters in config names + encoded_name = quote(kwargs.get('name'), safe='') + self._ds_url = f"{self._ds_url}&source={encoded_name}" + logger.debug(f"[Discord] 消息回调 URL: {self._ds_url}") intents = discord.Intents.default() intents.message_content = True @@ -59,6 +67,7 @@ class Discord: self._thread: Optional[threading.Thread] = None self._ready_event = threading.Event() self._user_dm_cache: Dict[str, discord.DMChannel] = {} + self._user_chat_mapping: Dict[str, str] = {} # userid -> chat_id mapping for reply targeting self._broadcast_channel = None self._bot_user_id: Optional[int] = None @@ -86,6 +95,9 @@ class Discord: if not self._should_process_message(message): return + # Update user-chat mapping for reply targeting + self._update_user_chat_mapping(str(message.author.id), str(message.channel.id)) + cleaned_text = self._clean_bot_mention(message.content or "") username = message.author.display_name or message.author.global_name or message.author.name payload = { @@ -112,6 +124,10 @@ class Discord: except Exception as e: logger.error(f"处理 Discord 交互响应失败:{e}") + # Update user-chat mapping for reply targeting + if interaction.user and interaction.channel: + self._update_user_chat_mapping(str(interaction.user.id), str(interaction.channel.id)) + username = (interaction.user.display_name or interaction.user.global_name or interaction.user.name) \ if interaction.user else None payload = { @@ -168,13 +184,19 @@ class Discord: original_message_id: Optional[Union[int, str]] = None, original_chat_id: Optional[str] = None, mtype: Optional['NotificationType'] = None) -> Optional[bool]: + logger.debug(f"[Discord] send_msg 被调用: userid={userid}, title={title[:50] if title else None}...") + logger.debug(f"[Discord] get_state() = {self.get_state()}, " + f"_ready_event.is_set() = {self._ready_event.is_set()}, " + f"_client = {self._client is not None}") if not self.get_state(): + logger.warning("[Discord] get_state() 返回 False,Bot 未就绪,无法发送消息") return False if not title and not text: logger.warn("标题和内容不能同时为空") return False try: + logger.debug(f"[Discord] 准备异步发送消息...") future = asyncio.run_coroutine_threadsafe( self._send_message(title=title, text=text, image=image, userid=userid, link=link, buttons=buttons, @@ -182,7 +204,9 @@ class Discord: original_chat_id=original_chat_id, mtype=mtype), self._loop) - return future.result(timeout=30) + result = future.result(timeout=30) + logger.debug(f"[Discord] 异步发送完成,结果: {result}") + return result except Exception as err: logger.error(f"发送 Discord 消息失败:{err}") return False @@ -254,7 +278,9 @@ class Discord: original_message_id: Optional[Union[int, str]], original_chat_id: Optional[str], mtype: Optional['NotificationType'] = None) -> bool: + logger.debug(f"[Discord] _send_message: userid={userid}, original_chat_id={original_chat_id}") channel = await self._resolve_channel(userid=userid, chat_id=original_chat_id) + logger.debug(f"[Discord] _resolve_channel 返回: {channel}, type={type(channel)}") if not channel: logger.error("未找到可用的 Discord 频道或私聊") return False @@ -264,11 +290,18 @@ class Discord: content = None if original_message_id and original_chat_id: + logger.debug(f"[Discord] 编辑现有消息: message_id={original_message_id}") return await self._edit_message(chat_id=original_chat_id, message_id=original_message_id, content=content, embed=embed, view=view) - await channel.send(content=content, embed=embed, view=view) - return True + logger.debug(f"[Discord] 发送新消息到频道: {channel}") + try: + await channel.send(content=content, embed=embed, view=view) + logger.debug("[Discord] 消息发送成功") + return True + except Exception as e: + logger.error(f"[Discord] 发送消息到频道失败: {e}") + return False async def _send_list_message(self, embeds: List[discord.Embed], userid: Optional[str], @@ -515,26 +548,54 @@ class Discord: return view async def _resolve_channel(self, userid: Optional[str] = None, chat_id: Optional[str] = None): - # 优先使用明确的聊天 ID + """ + Resolve the channel to send messages to. + Priority order: + 1. `chat_id` (original channel where user sent the message) - for contextual replies + 2. `userid` mapping (channel where user last sent a message) - for contextual replies + 3. Configured `_channel_id` (broadcast channel) - for system notifications + 4. Any available text channel in configured guild - fallback + 5. `userid` (DM) - for private conversations as a final fallback + """ + logger.debug(f"[Discord] _resolve_channel: userid={userid}, chat_id={chat_id}, " + f"_channel_id={self._channel_id}, _guild_id={self._guild_id}") + + # Priority 1: Use explicit chat_id (reply to the same channel where user sent message) if chat_id: + logger.debug(f"[Discord] 尝试通过 chat_id={chat_id} 获取原始频道") channel = self._client.get_channel(int(chat_id)) if channel: + logger.debug(f"[Discord] 通过 get_channel 找到频道: {channel}") return channel try: - return await self._client.fetch_channel(int(chat_id)) + channel = await self._client.fetch_channel(int(chat_id)) + logger.debug(f"[Discord] 通过 fetch_channel 找到频道: {channel}") + return channel except Exception as err: logger.warn(f"通过 chat_id 获取 Discord 频道失败:{err}") - # 私聊 + # Priority 2: Use user-chat mapping (reply to where the user last sent a message) if userid: - dm = await self._get_dm_channel(str(userid)) - if dm: - return dm + mapped_chat_id = self._get_user_chat_id(str(userid)) + if mapped_chat_id: + logger.debug(f"[Discord] 从用户映射获取 chat_id={mapped_chat_id}") + channel = self._client.get_channel(int(mapped_chat_id)) + if channel: + logger.debug(f"[Discord] 通过映射找到频道: {channel}") + return channel + try: + channel = await self._client.fetch_channel(int(mapped_chat_id)) + logger.debug(f"[Discord] 通过 fetch_channel 找到映射频道: {channel}") + return channel + except Exception as err: + logger.warn(f"通过映射的 chat_id 获取 Discord 频道失败:{err}") - # 配置的广播频道 + # Priority 3: Use configured broadcast channel (for system notifications) if self._broadcast_channel: + logger.debug(f"[Discord] 使用缓存的广播频道: {self._broadcast_channel}") return self._broadcast_channel if self._channel_id: + logger.debug(f"[Discord] 尝试通过配置的 _channel_id={self._channel_id} 获取频道") channel = self._client.get_channel(self._channel_id) if not channel: try: @@ -544,9 +605,11 @@ class Discord: channel = None self._broadcast_channel = channel if channel: + logger.debug(f"[Discord] 通过配置的频道ID找到频道: {channel}") return channel - # 按 Guild 寻找一个可用文本频道 + # Priority 4: Find any available text channel in guild (fallback) + logger.debug(f"[Discord] 尝试在 Guild 中寻找可用频道") target_guilds = [] if self._guild_id: guild = self._client.get_guild(self._guild_id) @@ -554,22 +617,47 @@ class Discord: target_guilds.append(guild) else: target_guilds = list(self._client.guilds) + logger.debug(f"[Discord] 目标 Guilds 数量: {len(target_guilds)}") for guild in target_guilds: for channel in guild.text_channels: if guild.me and channel.permissions_for(guild.me).send_messages: + logger.debug(f"[Discord] 在 Guild 中找到可用频道: {channel}") self._broadcast_channel = channel return channel + + # Priority 5: Fallback to DM (only if no channel available) + if userid: + logger.debug(f"[Discord] 回退到私聊: userid={userid}") + dm = await self._get_dm_channel(str(userid)) + if dm: + logger.debug(f"[Discord] 获取到私聊频道: {dm}") + return dm + else: + logger.debug(f"[Discord] 无法获取用户 {userid} 的私聊频道") + return None async def _get_dm_channel(self, userid: str) -> Optional[discord.DMChannel]: + logger.debug(f"[Discord] _get_dm_channel: userid={userid}") if userid in self._user_dm_cache: + logger.debug(f"[Discord] 从缓存获取私聊频道: {self._user_dm_cache.get(userid)}") return self._user_dm_cache.get(userid) try: - user_obj = self._client.get_user(int(userid)) or await self._client.fetch_user(int(userid)) + logger.debug(f"[Discord] 尝试获取/创建用户 {userid} 的私聊频道") + user_obj = self._client.get_user(int(userid)) + logger.debug(f"[Discord] get_user 结果: {user_obj}") if not user_obj: + user_obj = await self._client.fetch_user(int(userid)) + logger.debug(f"[Discord] fetch_user 结果: {user_obj}") + if not user_obj: + logger.debug(f"[Discord] 无法找到用户 {userid}") return None - dm = user_obj.dm_channel or await user_obj.create_dm() + dm = user_obj.dm_channel + logger.debug(f"[Discord] 用户现有 dm_channel: {dm}") + if not dm: + dm = await user_obj.create_dm() + logger.debug(f"[Discord] 创建新的 dm_channel: {dm}") if dm: self._user_dm_cache[userid] = dm return dm @@ -577,6 +665,25 @@ class Discord: logger.error(f"获取 Discord 私聊失败:{err}") return None + def _update_user_chat_mapping(self, userid: str, chat_id: str) -> None: + """ + Update user-chat mapping for reply targeting. + This ensures replies go to the same channel where the user sent the message. + :param userid: User ID + :param chat_id: Channel/Chat ID where the user sent the message + """ + if userid and chat_id: + self._user_chat_mapping[userid] = chat_id + logger.debug(f"[Discord] 更新用户频道映射: userid={userid} -> chat_id={chat_id}") + + def _get_user_chat_id(self, userid: str) -> Optional[str]: + """ + Get the chat ID where the user last sent a message. + :param userid: User ID + :return: Chat ID or None if not found + """ + return self._user_chat_mapping.get(userid) + def _should_process_message(self, message: discord.Message) -> bool: if isinstance(message.channel, discord.DMChannel): return True diff --git a/app/modules/slack/slack.py b/app/modules/slack/slack.py index 3931ecee..16f890c0 100644 --- a/app/modules/slack/slack.py +++ b/app/modules/slack/slack.py @@ -1,6 +1,7 @@ import re from threading import Lock from typing import List, Optional +from urllib.parse import quote import requests from slack_bolt import App @@ -42,7 +43,9 @@ class Slack: # 标记消息来源 if kwargs.get("name"): - self._ds_url = f"{self._ds_url}&source={kwargs.get('name')}" + # URL encode the source name to handle special characters + encoded_name = quote(kwargs.get('name'), safe='') + self._ds_url = f"{self._ds_url}&source={encoded_name}" # 注册消息响应 @slack_app.event("message") diff --git a/app/modules/telegram/telegram.py b/app/modules/telegram/telegram.py index c7588e95..86817b8a 100644 --- a/app/modules/telegram/telegram.py +++ b/app/modules/telegram/telegram.py @@ -2,7 +2,7 @@ import asyncio import re import threading from typing import Optional, List, Dict, Callable -from urllib.parse import urljoin +from urllib.parse import urljoin, quote from telebot import TeleBot, apihelper from telebot.types import BotCommand, InlineKeyboardMarkup, InlineKeyboardButton, InputMediaPhoto @@ -65,7 +65,9 @@ class Telegram: # 标记渠道来源 if kwargs.get("name"): - self._ds_url = f"{self._ds_url}&source={kwargs.get('name')}" + # URL encode the source name to handle special characters + encoded_name = quote(kwargs.get('name'), safe='') + self._ds_url = f"{self._ds_url}&source={encoded_name}" @_bot.message_handler(commands=['start', 'help']) def send_welcome(message):