fix(wechat-woc): 修复本账号消息循环拉取导致的死循环问题

1. 为微信企业微信适配器添加本账号发送消息过滤逻辑,分别在拉取适配器和流连接器适配器中实现
2. 修复拉取 worker 空消息场景下游标卡死问题,当有新游标且无有效消息时持久化新游标
3. 增加过滤日志便于排查消息丢失情况,统一本账号消息判断逻辑
This commit is contained in:
Kris 2026-07-10 18:30:21 +08:00
parent 713fcdcd8e
commit 3c26a68fa6
3 changed files with 87 additions and 1 deletions

View File

@ -176,6 +176,26 @@ class PullerWorker(BaseTransportWorker):
continue
if not result.messages:
# 适配器可能过滤了消息(如 wechat_woc 过滤本账号发送的回复),
# 此时 next_cursor 已基于全量消息前进。若不持久化,下次仍用旧
# cursor 拉取被过滤的消息,导致 cursor 卡死形成静默死循环
# (不触发 AgentRun 但无限轮询,且阻塞后续用户消息)。仅当
# next_cursor 非空且前进时持久化;无消息且 cursor 不变时保持
# 原行为(不写 DB避免空轮询频繁持久化。
if result.next_cursor is not None and result.next_cursor != cursor:
persisted = await self._saveCursor(
channel_type,
account_id,
result.next_cursor,
last_persisted_cursor,
error_trace_id,
expected_version=account_version,
)
if persisted:
cursor = result.next_cursor
last_persisted_cursor = result.next_cursor
# 持久化失败时不进入退避重试(无消息非错误),用旧 cursor
# 下次重新拉取,保证 at-least-once 语义。
task_info.backoff_attempt = 0
task_info.consecutive_successes += 1
if task_info.consecutive_successes == 1:

View File

@ -180,6 +180,23 @@ class WeChatWocPullerAdapter:
# create_time"严格区分,后者必须前进)
next_cursor = cursor
# 过滤本账号发送的消息is_sender 真值。bridge /api/messages/since
# 返回会话内全部消息,含系统经 outbound 发送的回复;若不过滤,这些
# 回复会被当作用户消息重新拉入,触发 AgentRun → 新回复 → 再被回拉,
# 形成无限循环。游标已基于全量消息计算过滤不影响前进C-1
original_count = len(messages)
messages = [m for m in messages if not _is_self_sent(m)]
skipped_self = original_count - len(messages)
if skipped_self > 0:
await self._logger.debug(
"woc poll filtered self-sent messages",
account_id=account_id,
trace_id=trace_id,
original_count=original_count,
filtered_count=skipped_self,
remaining_count=len(messages),
)
await self._logger.debug(
"woc poll completed",
account_id=account_id,
@ -286,4 +303,26 @@ class WeChatWocPullerAdapter:
raise DependencyError(dep="config", cause=exc) from exc
def _is_self_sent(msg: dict) -> bool:
"""判断消息是否为本账号发送(``is_sender`` 真值)。
bridge 契约返回 int ``0``/``1````0``=对方发送``1``=本账号发送
兼容 bool string 真值显式排除 ``0``/``"0"``/``False`` 避免字符串
误判导致用户消息被丢弃消息丢失比循环更严重
dict 输入返回 False透传下游保持原有单条失败语义避免在 poll
列表推导中抛异常导致整批消息丢失
@consistency: 无状态纯函数
"""
if not isinstance(msg, dict):
return False
is_sender = msg.get("is_sender")
if is_sender is None or is_sender is False or is_sender == 0:
return False
if isinstance(is_sender, str):
return is_sender.strip().lower() in {"1", "true", "yes"}
return bool(is_sender)
__all__ = ["WeChatWocPullerAdapter"]

View File

@ -180,7 +180,14 @@ class WeChatWocStreamConnectorAdapter:
while True:
# 缓冲队列有消息时优先返回,避免阻塞在读事件上
if not buffer.empty():
return await buffer.get()
msg = await buffer.get()
# 过滤本账号发送的消息is_sender 真值。bridge SSE 推送
# 含系统经 outbound 发送的回复;若不过滤,这些回复会被
# 当作用户消息重新处理,触发 AgentRun → 新回复 → 再被
# 推送,形成无限循环。游标由 sync 全局游标驱动,不受影响。
if _is_self_sent(msg):
continue
return msg
try:
event = await anext(events_iter)
@ -512,4 +519,24 @@ class WeChatWocStreamConnectorAdapter:
raise DependencyError(dep="config", cause=exc) from exc
def _is_self_sent(msg: dict) -> bool:
"""判断消息是否为本账号发送(``is_sender`` 真值)。
bridge 契约返回 int ``0``/``1````0``=对方发送``1``=本账号发送
兼容 bool string 真值显式排除 ``0``/``"0"``/``False`` 避免字符串
误判导致用户消息被丢弃 ``puller_adapter._is_self_sent`` 同逻辑
跨模块不 import 私有函数各自内联
@consistency: 无状态纯函数
"""
if not isinstance(msg, dict):
return False
is_sender = msg.get("is_sender")
if is_sender is None or is_sender is False or is_sender == 0:
return False
if isinstance(is_sender, str):
return is_sender.strip().lower() in {"1", "true", "yes"}
return bool(is_sender)
__all__ = ["WeChatWocStreamConnectorAdapter"]