优化微信同步超时兜底并下沉头像缓存预热

- 为 Msg/Sync 增加超时异常与主循环重试保护,避免启动阶段超时直接退出\n- 新增联系人头像缓存系统定时任务,启动时不再主动批量下载头像\n- 保留头像按需补下载能力,并补充详细中文注释
This commit is contained in:
Liu
2026-05-01 12:24:27 +08:00
parent 34adefa931
commit c3830d905e
5 changed files with 171 additions and 80 deletions

View File

@@ -60,6 +60,9 @@ class Robot:
self.nickname = None
self.alias = None
self.phone = None
# 连续同步超时计数单独保留,目的是区分“偶发网络抖动”和“服务端已经持续不可用”。
# 这样后续日志可以更明确地告诉我们当前是第几次连续超时,排障时不需要手工数日志。
self.ipad_sync_timeout_streak = 0
self.message_auto_revoke: MessageAutoRevoke = None
self.LOG.debug(f"DB+REDIS 连接池开始初始化")
# 使用单例模式获取实例
@@ -303,8 +306,11 @@ class Robot:
# await self.ipad_bot.send_text_message("filehelper", "ipad客户端启动成功")
count = 0
while True:
data = await self.ipad_bot.sync_message()
data = data.get("AddMsgs")
data_temp = await self._sync_ipad_messages_with_guard("处理堆积消息")
if data_temp is None:
continue
data = data_temp.get("AddMsgs")
if not data:
if count > 2:
break
@@ -324,17 +330,8 @@ class Robot:
# 开始处理消息
self.LOG.info("开始处理wechat_ipad消息")
while self.ipad_running:
try:
data_temp = await self.ipad_bot.sync_message()
except Exception as e:
self.LOG.error(f"获取新消息失败 {e}")
if "用户可能退出" in str(e):
self.LOG.error(f"用户可能退出: {e}")
self.email_sender.send_wechat_alert(self.config.email.get("alert_recipient"),
f"用户可能退出: {e}", self.wxid,
self.nickname)
await self.login_twice_auto_auth()
await asyncio.sleep(5)
data_temp = await self._sync_ipad_messages_with_guard("消息主循环")
if data_temp is None:
continue
data = data_temp.get("AddMsgs")
@@ -384,6 +381,55 @@ class Robot:
self.LOG.exception(f"wechat_ipad客户端运行出错: {e}")
self.ipad_running = False
async def _sync_ipad_messages_with_guard(self, phase: str) -> dict | None:
"""统一封装 wechat_ipad 的消息同步调用。
设计说明:
1. 启动阶段清空堆积消息和运行阶段实时拉消息,本质上调用的是同一个 `/api/Msg/Sync`
2. 过去两处各自直接调用 `sync_message()`,导致启动阶段一旦超时就会把整个主循环打断;
3. 现在把重试、连续超时计数、掉线自愈放在一起,后续如果策略要调整,只改这一处即可。
"""
try:
data = await self.ipad_bot.sync_message()
if self.ipad_sync_timeout_streak > 0:
# 这里在恢复成功时主动打一条恢复日志,方便和前面的连续超时告警配对查看。
self.LOG.info(
f"{phase}同步消息恢复正常,已清空连续超时计数: {self.ipad_sync_timeout_streak}"
)
self.ipad_sync_timeout_streak = 0
return data
except wechat_ipad.RequestTimeoutError as timeout_error:
# 对同步超时做“警告级别 + 连续计数”处理,而不是直接当致命错误退出:
# 1. 局域网环境下偶发抖动、服务端短暂卡顿都可能触发超时;
# 2. 这类问题大多数可通过下一轮重试自动恢复;
# 3. 只有保留连续次数,我们才能快速判断是偶发还是持续故障。
self.ipad_sync_timeout_streak += 1
self.LOG.warning(
f"{phase}同步消息超时,第 {self.ipad_sync_timeout_streak} 次连续超时: {timeout_error}"
)
except Exception as e:
if self.ipad_sync_timeout_streak > 0:
# 非超时异常说明故障类型已经变化,先把超时计数归零,避免后续日志语义混乱。
self.LOG.warning(
f"{phase}同步消息异常类型已变化,重置连续超时计数: {self.ipad_sync_timeout_streak}"
)
self.ipad_sync_timeout_streak = 0
self.LOG.error(f"{phase}获取新消息失败: {e}")
if "用户可能退出" in str(e):
self.LOG.error(f"用户可能退出: {e}")
self.email_sender.send_wechat_alert(
self.config.email.get("alert_recipient"),
f"用户可能退出: {e}",
self.wxid,
self.nickname
)
await self.login_twice_auto_auth()
# 这里统一等待 5 秒再重试,避免在服务端异常时进入高频空转。
await asyncio.sleep(5)
return None
# 在类里直接写一个内联 async 方法(不额外抽取新的对外方法)
async def _process_with_semaphore(self, wxmsg):
@@ -1087,3 +1133,21 @@ class Robot:
self.message_storage.write_to_db()
except Exception as e:
self.LOG.error(f"write_to_db error{e}")
async def sync_contact_avatar_cache(self) -> None:
"""系统级定时任务:增量同步联系人头像缓存。
说明:
1. 头像缓存预热从登录启动链路挪到这里,避免机器人刚上线时就批量下载头像;
2. `ContactManager.sync_avatar_cache()` 仍是同步 I/O因此这里用 `asyncio.to_thread` 丢到线程池执行;
3. 这样既保留定时批处理能力,也不会阻塞 async_job 所在的事件循环。
"""
try:
stats = await asyncio.to_thread(
self.contact_manager.sync_avatar_cache,
"system_job:sync_contact_avatar_cache",
)
self.LOG.info(f"联系人头像缓存定时同步完成: {stats}")
except Exception as e:
self.LOG.error(f"sync_contact_avatar_cache error: {e}")
raise