diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe033d..363a801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ # 更新日志 (CHANGELOG) +## [v4.7.0] - fix(AutoScheduler): 修复多平台环境下群列表获取异常,优化 Discord 权限说明 + +* **🛠️ 健壮性增强**: 优化 `AutoScheduler` 启动逻辑,强制进行 Bot 实例自动发现,确保冷启动时能正确获取群列表。 +* **🔌 平台优化**: 修复多平台(如 Discord)下适配器识别不稳定的问题,优先使用 `BotManager` 缓存的适配器。 +* **📕 文档更新**: 在 `README.md` 中补充了 Discord 平台必需的 “查看消息历史记录” (Read Message History) 权限说明。 + ## [v4.6.9] - fix(AutoScheduler): 增加友好提示,增强性处理,优化用户体验 -* **🛠️ 健壮性增强**: 增加友好提示,增强性处理,优化用户体验 --- diff --git a/README.md b/README.md index de6ba4e..6a2c409 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,9 @@ retro_futurism 模板效果图 > [!CAUTION] > **必需条件**: > - 已配置 LLM 提供商(用于智能分析) -> - QQ 平台适配器 +> - 平台适配器(OneBot, Discord, Telegram 等) +> - **Discord 平台特别说明**:机器人需要在频道中拥有 **“读取消息历史记录” (Read Message History)** 权限,否则分析将失败。 + ## 注意事项 diff --git a/metadata.yaml b/metadata.yaml index e80346d..ceb78ed 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -14,6 +14,6 @@ help: | # 插件的帮助信息 命令: /群分析 [天数] - 分析群聊活动 /分析设置 [操作] - 管理设置(enable/disable/status/test) -version: v4.6.9 # 插件版本号。格式:v1.1.1 或者 v1.1 +version: v4.7.0 # 插件版本号。格式:v1.1.1 或者 v1.1 author: SXP-Simon # 作者 repo: https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis # 插件的仓库地址 diff --git a/src/infrastructure/platform/adapters/onebot_adapter.py b/src/infrastructure/platform/adapters/onebot_adapter.py index 8ad7e36..3dfe1b9 100644 --- a/src/infrastructure/platform/adapters/onebot_adapter.py +++ b/src/infrastructure/platform/adapters/onebot_adapter.py @@ -23,18 +23,24 @@ from ..base import PlatformAdapter class OneBotAdapter(PlatformAdapter): + platform_name = "onebot" + """OneBot v11 协议适配器""" # QQ 头像 URL 模板 USER_AVATAR_TEMPLATE = "https://q1.qlogo.cn/g?b=qq&nk={user_id}&s={size}" - USER_AVATAR_HD_TEMPLATE = "https://q.qlogo.cn/headimg_dl?dst_uin={user_id}&spec={size}&img_type=jpg" + USER_AVATAR_HD_TEMPLATE = ( + "https://q.qlogo.cn/headimg_dl?dst_uin={user_id}&spec={size}&img_type=jpg" + ) GROUP_AVATAR_TEMPLATE = "https://p.qlogo.cn/gh/{group_id}/{group_id}/{size}/" - + AVAILABLE_SIZES = [40, 100, 140, 160, 640] def __init__(self, bot_instance: Any, config: dict = None): super().__init__(bot_instance, config) - self.bot_self_ids = [str(id) for id in config.get("bot_qq_ids", [])] if config else [] + self.bot_self_ids = ( + [str(id) for id in config.get("bot_qq_ids", [])] if config else [] + ) def _init_capabilities(self) -> PlatformCapabilities: return ONEBOT_V11_CAPABILITIES @@ -90,7 +96,9 @@ class OneBotAdapter(PlatformAdapter): except Exception: return [] - def _convert_message(self, raw_msg: dict, group_id: str) -> Optional[UnifiedMessage]: + def _convert_message( + self, raw_msg: dict, group_id: str + ) -> Optional[UnifiedMessage]: """将 OneBot 消息转换为统一格式""" try: sender = raw_msg.get("sender", {}) @@ -109,56 +117,70 @@ class OneBotAdapter(PlatformAdapter): if seg_type == "text": text = seg_data.get("text", "") text_parts.append(text) - contents.append(MessageContent(type=MessageContentType.TEXT, text=text)) + contents.append( + MessageContent(type=MessageContentType.TEXT, text=text) + ) elif seg_type == "image": - contents.append(MessageContent( - type=MessageContentType.IMAGE, - url=seg_data.get("url", seg_data.get("file", "")) - )) + contents.append( + MessageContent( + type=MessageContentType.IMAGE, + url=seg_data.get("url", seg_data.get("file", "")), + ) + ) elif seg_type == "at": - contents.append(MessageContent( - type=MessageContentType.AT, - at_user_id=str(seg_data.get("qq", "")) - )) + contents.append( + MessageContent( + type=MessageContentType.AT, + at_user_id=str(seg_data.get("qq", "")), + ) + ) elif seg_type in ("face", "mface", "bface", "sface"): - contents.append(MessageContent( - type=MessageContentType.EMOJI, - emoji_id=str(seg_data.get("id", "")), - raw_data={"face_type": seg_type} - )) + contents.append( + MessageContent( + type=MessageContentType.EMOJI, + emoji_id=str(seg_data.get("id", "")), + raw_data={"face_type": seg_type}, + ) + ) elif seg_type == "reply": - contents.append(MessageContent( - type=MessageContentType.REPLY, - raw_data={"reply_id": seg_data.get("id", "")} - )) + contents.append( + MessageContent( + type=MessageContentType.REPLY, + raw_data={"reply_id": seg_data.get("id", "")}, + ) + ) elif seg_type == "forward": - contents.append(MessageContent( - type=MessageContentType.FORWARD, - raw_data=seg_data - )) + contents.append( + MessageContent( + type=MessageContentType.FORWARD, raw_data=seg_data + ) + ) elif seg_type == "record": - contents.append(MessageContent( - type=MessageContentType.VOICE, - url=seg_data.get("url", seg_data.get("file", "")) - )) + contents.append( + MessageContent( + type=MessageContentType.VOICE, + url=seg_data.get("url", seg_data.get("file", "")), + ) + ) elif seg_type == "video": - contents.append(MessageContent( - type=MessageContentType.VIDEO, - url=seg_data.get("url", seg_data.get("file", "")) - )) + contents.append( + MessageContent( + type=MessageContentType.VIDEO, + url=seg_data.get("url", seg_data.get("file", "")), + ) + ) else: - contents.append(MessageContent( - type=MessageContentType.UNKNOWN, - raw_data=seg - )) + contents.append( + MessageContent(type=MessageContentType.UNKNOWN, raw_data=seg) + ) reply_to = None for c in contents: @@ -185,7 +207,7 @@ class OneBotAdapter(PlatformAdapter): def convert_to_raw_format(self, messages: List[UnifiedMessage]) -> List[dict]: """ 将统一消息格式转换为 OneBot 原生格式。 - + 用于与现有分析器的向后兼容。 """ raw_messages = [] @@ -194,26 +216,46 @@ class OneBotAdapter(PlatformAdapter): message_chain = [] for content in msg.contents: if content.type == MessageContentType.TEXT: - message_chain.append({"type": "text", "data": {"text": content.text or ""}}) + message_chain.append( + {"type": "text", "data": {"text": content.text or ""}} + ) elif content.type == MessageContentType.IMAGE: - message_chain.append({"type": "image", "data": {"url": content.url or ""}}) + message_chain.append( + {"type": "image", "data": {"url": content.url or ""}} + ) elif content.type == MessageContentType.AT: - message_chain.append({"type": "at", "data": {"qq": content.at_user_id or ""}}) + message_chain.append( + {"type": "at", "data": {"qq": content.at_user_id or ""}} + ) elif content.type == MessageContentType.EMOJI: - face_type = content.raw_data.get("face_type", "face") if content.raw_data else "face" - message_chain.append({"type": face_type, "data": {"id": content.emoji_id or ""}}) + face_type = ( + content.raw_data.get("face_type", "face") + if content.raw_data + else "face" + ) + message_chain.append( + {"type": face_type, "data": {"id": content.emoji_id or ""}} + ) elif content.type == MessageContentType.REPLY: - reply_id = content.raw_data.get("reply_id", "") if content.raw_data else "" + reply_id = ( + content.raw_data.get("reply_id", "") if content.raw_data else "" + ) message_chain.append({"type": "reply", "data": {"id": reply_id}}) elif content.type == MessageContentType.FORWARD: - message_chain.append({"type": "forward", "data": content.raw_data or {}}) + message_chain.append( + {"type": "forward", "data": content.raw_data or {}} + ) elif content.type == MessageContentType.VOICE: - message_chain.append({"type": "record", "data": {"url": content.url or ""}}) + message_chain.append( + {"type": "record", "data": {"url": content.url or ""}} + ) elif content.type == MessageContentType.VIDEO: - message_chain.append({"type": "video", "data": {"url": content.url or ""}}) + message_chain.append( + {"type": "video", "data": {"url": content.url or ""}} + ) elif content.type == MessageContentType.UNKNOWN and content.raw_data: message_chain.append(content.raw_data) - + raw_msg = { "message_id": msg.message_id, "time": msg.timestamp, @@ -224,11 +266,11 @@ class OneBotAdapter(PlatformAdapter): }, "message": message_chain, "group_id": msg.group_id, - "raw_message": msg.text_content, # 添加 raw_message 兼容字段 - "user_id": msg.sender_id, # 添加 user_id 兼容字段 + "raw_message": msg.text_content, # 添加 raw_message 兼容字段 + "user_id": msg.sender_id, # 添加 user_id 兼容字段 } raw_messages.append(raw_msg) - + return raw_messages # ==================== IMessageSender ==================== @@ -344,13 +386,15 @@ class OneBotAdapter(PlatformAdapter): members = [] for m in result or []: - members.append(UnifiedMember( - user_id=str(m.get("user_id", "")), - nickname=m.get("nickname", ""), - card=m.get("card", "") or None, - role=m.get("role", "member"), - join_time=m.get("join_time"), - )) + members.append( + UnifiedMember( + user_id=str(m.get("user_id", "")), + nickname=m.get("nickname", ""), + card=m.get("card", "") or None, + role=m.get("role", "member"), + join_time=m.get("join_time"), + ) + ) return members except Exception: return [] @@ -406,11 +450,13 @@ class OneBotAdapter(PlatformAdapter): try: async with aiohttp.ClientSession() as session: - async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp: + async with session.get( + url, timeout=aiohttp.ClientTimeout(total=5) + ) as resp: if resp.status == 200: data = await resp.read() - b64 = base64.b64encode(data).decode('utf-8') - content_type = resp.headers.get('Content-Type', 'image/png') + b64 = base64.b64encode(data).decode("utf-8") + content_type = resp.headers.get("Content-Type", "image/png") return f"data:{content_type};base64,{b64}" except Exception: pass diff --git a/src/scheduler/auto_scheduler.py b/src/scheduler/auto_scheduler.py index dbe063b..02f1197 100644 --- a/src/scheduler/auto_scheduler.py +++ b/src/scheduler/auto_scheduler.py @@ -8,6 +8,7 @@ import weakref from apscheduler.triggers.cron import CronTrigger +from ..infrastructure.platform.factory import PlatformAdapterFactory from ..utils.logger import logger from ..core.message_sender import MessageSender @@ -520,27 +521,21 @@ class AutoScheduler: from ..infrastructure.platform.factory import PlatformAdapterFactory # 强制刷新一次 Bot 实例,确保最新的 Bot 被发现 - # 这对于 AutoScheduler 这种定时任务很重要,因为 BotManager 可能懒加载 if hasattr(self.bot_manager, "auto_discover_bot_instances"): try: - # 注意:这里需要在 async 上下文中调用,且 auto_discover_bot_instances 是 async 的 - # 但我们不想在这里阻塞太久或引发循环调用问题。 - # 鉴于 _get_all_groups 是 async 的,直接 await 是安全的。 await self.bot_manager.auto_discover_bot_instances() except Exception as e: logger.warning(f"[AutoScheduler] Auto-discovery failed: {e}") - logger.info( - f"[AutoScheduler] Bot instances: {list(self.bot_manager._bot_instances.keys())}" - ) - logger.info( - f"[AutoScheduler] Adapters: {list(self.bot_manager._adapters.keys())}" + bot_ids = list(self.bot_manager._bot_instances.keys()) + adapter_ids = list(self.bot_manager._adapters.keys()) + + # 调试模式下记录详细信息,INFO级别仅显示概览 + logger.debug( + f"[AutoScheduler] Bot instances: {bot_ids}, Adapters: {adapter_ids}" ) - if ( - not hasattr(self.bot_manager, "_bot_instances") - or not self.bot_manager._bot_instances - ): + if not bot_ids: logger.warning("[AutoScheduler] No bot instances found after discovery.") return [] @@ -576,13 +571,19 @@ class AutoScheduler: for group_id in groups: all_groups.add((platform_id, str(group_id))) - p_name = getattr( - adapter, "platform_name", platform_name or "unknown" - ) + # 获取可读的平台名称 + p_name = getattr(adapter, "platform_name", None) + if not p_name: + p_name = ( + self.bot_manager._detect_platform_name(bot_instance) + or "unknown" + ) + logger.info( f"平台 {platform_id} ({p_name}) 成功获取 {len(groups)} 个群组" ) continue + except Exception as e: logger.warning(f"适配器 {platform_id} 获取群列表失败: {e}")