diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c17e0..75ab4b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,18 @@ # 更新日志 (CHANGELOG) -## [v4.7.13] - fix(增量分析)解决增量分析传参错误导致用户称号失效的问题 +## [v4.7.14] - fix(retry): 尝试增强重试管理器,正确获取 Bot user_id ,避免重试地狱和重复发送 * **🛠️ 健壮性增强**: 尝试增强重试管理器,避免重试地狱和重复发送(有待进一步完善) -* **🐛 Bug 修复**: 解决增量分析传参错误导致用户称号失效的问题 + ---
📋 点击查看历史更新日志 +## [v4.7.13] - fix(增量分析)解决增量分析传参错误导致用户称号失效的问题 +* **🛠️ 健壮性增强**: 尝试增强重试管理器,避免重试地狱和重复发送(有待进一步完善) +* **🐛 Bug 修复**: 解决增量分析传参错误导致用户称号失效的问题 + ## [v4.7.11] - feat(base64): 新增可选 Base64 图片传输开关,默认启用 * **✨ 新功能**: 新增可选 Base64 图片传输开关,默认启用,兼容更多环境。 * **🐛 Bug 修复**: 修复图片路径处理逻辑,处理绝对路径和相对路径的文件路径转换 diff --git a/metadata.yaml b/metadata.yaml index eb08726..b46763d 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,7 +1,7 @@ name: astrbot_plugin_qq_group_daily_analysis # 这是你的插件的唯一识别名。 display_name: 群分析总结插件 # 插件的显示名称 desc: "[多平台接入开发中] 群日常分析总结插件 - 支持 QQ (aiocqhttp)、Telegram、Discord;生成精美的群聊分析报告,支持话题分析、用户形象、群聊圣经等功能" # 插件简短描述 -version: v4.7.13 # 插件版本号。格式:v1.1.1 或者 v1.1 +version: v4.7.14 # 插件版本号。格式:v1.1.1 或者 v1.1 author: SXP-Simon # 作者 astrbot_version: ">=4.16.0" support_platforms: diff --git a/src/infrastructure/platform/adapters/onebot_adapter.py b/src/infrastructure/platform/adapters/onebot_adapter.py index 6a0c034..5413d84 100644 --- a/src/infrastructure/platform/adapters/onebot_adapter.py +++ b/src/infrastructure/platform/adapters/onebot_adapter.py @@ -53,15 +53,14 @@ class OneBotAdapter(PlatformAdapter): def __init__(self, bot_instance: Any, config: dict | None = None): """ 初始化 OneBot 适配器。 - - Args: - bot_instance (Any): 外部传入的机器人对象 - config (dict, optional): 插件配置,用于提取机器人自身的 QQ 号供过滤用 """ super().__init__(bot_instance, config) + # 支持从多个潜在的配置键中提取机器人 ID self.bot_self_ids = ( - [str(id) for id in config.get("bot_qq_ids", [])] if config else [] + [str(id) for id in config.get("bot_self_ids", [])] if config else [] ) + if not self.bot_self_ids and config: + self.bot_self_ids = [str(id) for id in config.get("bot_qq_ids", [])] def _init_capabilities(self) -> PlatformCapabilities: """返回预定义的 OneBot v11 能力集。""" @@ -544,7 +543,7 @@ class OneBotAdapter(PlatformAdapter): history = await self.bot.call_action( "get_group_msg_history", group_id=int(group_id), - count=50, # 只检查最近 50 条消息,足够覆盖大多数情况 + count=100, # [针对重复检查优化] 提高扫描深度,覆盖大群高频刷屏的情况 ) if not history or "messages" not in history: @@ -557,7 +556,36 @@ class OneBotAdapter(PlatformAdapter): import time now = time.time() - self_id = str(getattr(self.bot, "self_id", "")) + # 1. 优先从内存缓存中获取机器人 ID + self_id = self.bot_self_ids[0] if self.bot_self_ids else "" + + # 2. 如果列表为空,尝试反射实例属性 + if not self_id: + self_id = ( + str(getattr(self.bot, "self_id", "")) + or str(getattr(self.bot, "uin", "")) + or str(getattr(self.bot, "user_id", "")) + ) + + # 3. [兜底方案] 仍未获取到,通过 API 向 OneBot 服务端请求 + if not self_id: + try: + login_info = await self.bot.call_action("get_login_info") + if login_info and "user_id" in login_info: + self_id = str(login_info["user_id"]) + # 更新缓存,下次无需重复请求 + if self_id not in self.bot_self_ids: + self.bot_self_ids.append(self_id) + logger.info(f"[OneBot] 成功通过 API 获取到机器人 ID: {self_id}") + except Exception as e: + logger.debug( + f"[OneBot] was_image_sent_recently: get_login_info API 调用失败: {e}" + ) + + if not self_id: + logger.warning( + "[OneBot] was_image_sent_recently: 无法确定机器人 ID,历史回显校验可能不准确" + ) for msg in reversed(messages): msg_time = msg.get("time", 0) diff --git a/src/infrastructure/scheduler/retry.py b/src/infrastructure/scheduler/retry.py index 55b8b93..38e54c8 100644 --- a/src/infrastructure/scheduler/retry.py +++ b/src/infrastructure/scheduler/retry.py @@ -113,10 +113,8 @@ class RetryManager: try: task: RetryTask = await self.queue.get() - # 如果该群已经在处理中,不再重复处理(双重保险) - if task.group_id in self._active_groups: - self.queue.task_done() - continue + # 【修复】去掉原本在这里的 group_id in self._active_groups 判断 + # 因为重新排队的任务本身就在 active_groups 中,会导致任务死在队列里被彻底丢弃 # 启动非阻塞的延迟执行协程 asyncio.create_task(self._run_task_with_delay(task)) @@ -130,13 +128,14 @@ class RetryManager: async def _run_task_with_delay(self, task: RetryTask): """异步执行带延迟的单体重试任务""" - # 锁定该群,防止其他重试任务进入 - if task.group_id in self._active_groups: + # 锁定该群,防止其他“新”重试任务进入。 + # 如果是重试任务(retry_count > 0),它已经在队列循环中,之前已经释放过锁。 + if task.group_id in self._active_groups and task.retry_count == 0: return self._active_groups.add(task.group_id) try: - # 1. 延迟策略:指数回落 + 观察期 (应对 OneBot 假超时) + # 1. 策略计算:指数回落 + 抖动 jitter = random.uniform(2, 8) delay = 20 * (2**task.retry_count) + jitter @@ -144,19 +143,23 @@ class RetryManager: logger.info( f"[RetryManager] 群 {task.group_id} 启动 {delay:.1f}s 重试观察期..." ) + else: + logger.info( + f"[RetryManager] 群 {task.group_id} 准备第 {task.retry_count + 1} 轮重试,退避 {delay:.1f}s..." + ) await asyncio.sleep(delay) if not self.running: return - # 【核心判定逻辑】:睡醒后先别急着发,去群里看看那张“疑似失败”的图是不是其实已经出来了 + # 【真相检查 1】:睡醒后先核实群里图片是不是其实已经出来了 adapter = self.bot_manager.get_adapter(task.platform_id) if adapter and hasattr(adapter, "was_image_sent_recently"): - # 检查过去 3 分钟内的消息回显 (覆盖初发和之前的重试) - if await adapter.was_image_sent_recently(task.group_id, seconds=180): + # 检查过去 5 分钟内的消息回显 (覆盖初发和之前的重试) + if await adapter.was_image_sent_recently(task.group_id, seconds=300): logger.info( - f"[RetryManager] 根据消息回显判断,群 {task.group_id} 的图片报告已成功送达。取消后续重试。" + f"[RetryManager] [拦截] 根据历史回显,群 {task.group_id} 的图片已成功送达。取消本次重试。" ) return @@ -164,16 +167,16 @@ class RetryManager: success = await self._process_task(task) if success: - logger.info(f"[RetryManager] 群 {task.group_id} 重试发送成功") + logger.info(f"[RetryManager] 群 {task.group_id} 重试流程圆满完成") else: # 3. 失败后续处理 task.retry_count += 1 if task.retry_count < task.max_retries: - # 释放锁,以便下次取到时能重新进入观察期 - self._active_groups.discard(task.group_id) + # 将任务重新放回队列。 + # 注意:锁会在 finally 释放,这样下一个 worker 就能拉取到它并进入睡眠。 await self.queue.put(task) logger.warning( - f"[RetryManager] 群 {task.group_id} 本轮重试失败,准备进入第 {task.retry_count + 1} 轮..." + f"[RetryManager] 群 {task.group_id} 本轮调用返回失败,已排期下一轮..." ) else: logger.error( @@ -181,9 +184,9 @@ class RetryManager: ) await self._send_fallback_text(task) except Exception as e: - logger.error(f"[RetryManager] 重试协程执行异常 (群 {task.group_id}): {e}") + logger.error(f"[RetryManager] 重试协程发生意外: {e}", exc_info=True) finally: - # 确保最终释放该群的重试锁 + # 释放群锁 if task.group_id in self._active_groups: self._active_groups.discard(task.group_id) @@ -281,9 +284,18 @@ class RetryManager: ) return False - # 3. 发送图片 (通过统一适配器接口) + # 3. 【临界检查 2】发送图片前最后一次复核 (针对渲染耗时极长产生的盲窗) + # 例如渲染 10s 期间图片出来了,这里可以最后贴身拦截一次 + if adapter and hasattr(adapter, "was_image_sent_recently"): + if await adapter.was_image_sent_recently(task.group_id, seconds=120): + logger.info( + f"[RetryManager] [临界拦截] 渲染完成后检测到群 {task.group_id} 已有报告。拦截重复发送。" + ) + return True + + # 4. 执行实际发送 logger.info( - f"[RetryManager] 正在向群 {task.group_id} 发送重试图片 (Adapter: {type(adapter).__name__})..." + f"[RetryManager] 正在向群 {task.group_id} 发送回补图片 (Adapter: {type(adapter).__name__})..." ) # 注意:某些适配器可能需要 URL,某些需要 Base64。