问题描述
反向 WebSocket(OneBot v11,X-Client-Role: universal)客户端因网络抖动重连后(相同 X-Self-ID),AstrBot 的事件通道正常(消息事件持续上报),但所有主动发送消息(回复、插件推送等)立即失败,且无法自愈,直到重启 AstrBot 才恢复。
失败特征:调用 send_group_msg / send_private_msg 时瞬间抛出 ApiNotAvailable,其 str() 为空,日志表现为空错误(如「发送失败: 」「主动推送发送失败: ..., error=」),失败发生在事件到达后 10ms 量级,无网络往返。
根因
AstrBot 适配器使用的反向 WS 生命周期存在竞态:连接注册表按 X-Self-ID 为键。客户端重连时,新连接覆盖旧条目;随后旧连接的服务端处理器延迟收尾(半开 TCP 场景可达数分钟),收尾时无条件删除该键——实际删掉的是新连接的注册。事件通道按连接对象管理(set + discard)不受影响,因此表现为「收得到、发不出」,且永久失效直至重启。
时序:
- 连接 A(
X-Self-ID: 123)注册
- 连接 A 断网,客户端 ping 超时后重连
- 连接 B(同
X-Self-ID)注册,覆盖 A
- A 的服务端处理器最终收尾,按键删除,删掉 B 的注册
- 此后所有主动发送查表失败 →
ApiNotAvailable
修复方案
在 astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py 内按连接对象身份清理注册(已在生产环境验证有效,观察期零失败):
import aiocqhttp
from quart import websocket
def _remove_wsr_api_client(self) -> None:
self_id = websocket.headers['X-Self-ID']
ws = websocket._get_current_object()
# 仅清理本连接的注册;同 self_id 可能已被新连接覆盖
if self._wsr_api_clients.get(self_id) is ws:
del self._wsr_api_clients[self_id]
aiocqhttp.CQHttp._remove_wsr_api_client = _remove_wsr_api_client
复现条件
- 反向 WS(universal 角色)接入,客户端以相同
X-Self-ID 重连(常见 OneBot v11 实现均如此)
- 旧连接在服务端延迟收尾(半开 TCP 场景,收敛可达数分钟)
- 触发一次后发送通道即永久失效,只能重启恢复
环境
- AstrBot 4.28.1(docker 部署),Python 3.12,反向 WS universal 角色
问题描述
反向 WebSocket(OneBot v11,
X-Client-Role: universal)客户端因网络抖动重连后(相同X-Self-ID),AstrBot 的事件通道正常(消息事件持续上报),但所有主动发送消息(回复、插件推送等)立即失败,且无法自愈,直到重启 AstrBot 才恢复。失败特征:调用
send_group_msg/send_private_msg时瞬间抛出ApiNotAvailable,其str()为空,日志表现为空错误(如「发送失败: 」「主动推送发送失败: ..., error=」),失败发生在事件到达后 10ms 量级,无网络往返。根因
AstrBot 适配器使用的反向 WS 生命周期存在竞态:连接注册表按
X-Self-ID为键。客户端重连时,新连接覆盖旧条目;随后旧连接的服务端处理器延迟收尾(半开 TCP 场景可达数分钟),收尾时无条件删除该键——实际删掉的是新连接的注册。事件通道按连接对象管理(set +discard)不受影响,因此表现为「收得到、发不出」,且永久失效直至重启。时序:
X-Self-ID: 123)注册X-Self-ID)注册,覆盖 AApiNotAvailable修复方案
在
astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py内按连接对象身份清理注册(已在生产环境验证有效,观察期零失败):复现条件
X-Self-ID重连(常见 OneBot v11 实现均如此)环境