fix(retry): 尝试增强重试管理器,采用 get_login_info 兜底增强获取 Bot user_id ,避免重试地狱和重复发送

This commit is contained in:
SXP-Simon
2026-02-24 00:17:09 +08:00
parent 412ac5f870
commit 417b88ffe9
4 changed files with 73 additions and 29 deletions
+6 -2
View File
@@ -1,14 +1,18 @@
# 更新日志 (CHANGELOG)
## [v4.7.13] - fix(增量分析)解决增量分析传参错误导致用户称号失效的问题
## [v4.7.14] - fix(retry): 尝试增强重试管理器,正确获取 Bot user_id ,避免重试地狱和重复发送
* **🛠️ 健壮性增强**: 尝试增强重试管理器,避免重试地狱和重复发送(有待进一步完善)
* **🐛 Bug 修复**: 解决增量分析传参错误导致用户称号失效的问题
---
<details>
<summary>📋 点击查看历史更新日志</summary>
## [v4.7.13] - fix(增量分析)解决增量分析传参错误导致用户称号失效的问题
* **🛠️ 健壮性增强**: 尝试增强重试管理器,避免重试地狱和重复发送(有待进一步完善)
* **🐛 Bug 修复**: 解决增量分析传参错误导致用户称号失效的问题
## [v4.7.11] - feat(base64): 新增可选 Base64 图片传输开关,默认启用
* **✨ 新功能**: 新增可选 Base64 图片传输开关,默认启用,兼容更多环境。
* **🐛 Bug 修复**: 修复图片路径处理逻辑,处理绝对路径和相对路径的文件路径转换
+1 -1
View File
@@ -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:
@@ -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)
+31 -19
View File
@@ -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。