Runtime environment
LangBot v4.10.x / latest, Docker deployment, Community Edition, QQ Official Bot API (qqofficial).
Observed in a Docker runtime with the QQ Official adapter while handling Agent / Skill / streaming / multi-part reply output.
Deployment version
Community Edition
Exception
When LangBot sends multiple replies or stream chunks for the same inbound QQ Official message, QQ may reject subsequent sends with duplicate-message detection:
ValueError: QQ private send failed: status=400,
body={
'message': '消息被去重,请检查请求msgseq',
'code': 40054005,
'err_code': 40054005,
'trace_id': '...'
},
content_len=...,
msg_id=ROBOT1.0_...,
msg_seq=1
Expected behavior:
For multiple outbound messages or stream chunks associated with the same inbound QQ msg_id, LangBot should allocate a fresh msg_seq for every send attempt.
same inbound msg_id -> msg_seq: 1, 2, 3, ...
This should apply to normal text replies, mixed message chains that are split into multiple sends, rich media sends, streaming reply chunks, and retry attempts after failed sends.
Actual behavior:
In multi-part reply scenarios, the QQ Official adapter may reuse msg_seq=1 or omit proper per-reply msg_seq allocation. As a result, QQ Official API treats later sends as duplicate messages and returns:
40054005 消息被去重,请检查请求msgseq
Suspected code locations:
src/langbot/libs/qq_official_api/api.py
src/langbot/pkg/platform/sources/qqofficial.py
Possible related areas:
send_private_text_msg
send_group_text_msg
send_channle_group_text_msg
send_channle_private_text_msg
_send_media_msg
send_stream_msg
reply_message_chunk
In the current behavior, stream replies appear especially likely to trigger the issue because msg_seq can remain fixed while index changes.
Suggested fix direction:
Introduce a QQ Official reply sequence allocator keyed by the inbound source msg_id.
from collections import defaultdict
import asyncio
class QQOfficialClient:
def __init__(...):
...
self._reply_msg_seq = defaultdict(int)
self._reply_msg_seq_lock = asyncio.Lock()
async def next_reply_msg_seq(self, msg_id: str | None) -> int:
if not msg_id:
# fallback for proactive messages or unsupported contexts
self._msg_seq_counter += 1
return self._msg_seq_counter
async with self._reply_msg_seq_lock:
self._reply_msg_seq[str(msg_id)] += 1
return self._reply_msg_seq[str(msg_id)]
Then every outbound send associated with an inbound msg_id should call this allocator:
body = {
"content": content,
"msg_type": 0,
"msg_id": msg_id,
"msg_seq": await self.next_reply_msg_seq(msg_id),
}
For stream replies, index can continue to represent the stream chunk index, but msg_seq should also advance for each HTTP request:
chunk 0 -> index=0, msg_seq=1
chunk 1 -> index=1, msg_seq=2
chunk 2 -> index=2, msg_seq=3
Why this fix is needed:
QQ Official API uses msg_seq for duplicate-message detection. If multiple sends under the same source msg_id reuse the same msg_seq, later messages can be rejected as duplicates.
A per-source-message msg_seq allocator would make QQ Official delivery safer for streaming, Agent, Skill, rich media, and other multi-part reply scenarios.
Reproduction steps
- Configure a QQ Official bot.
- Enable a mode that can produce multiple outbound replies for one inbound message, such as streaming output, Agent output, or Skill/tool-assisted output.
- Send one user message that causes multiple chunks or multiple reply sends.
- Observe that the first send may succeed, while subsequent sends may fail with:
40054005 消息被去重,请检查请求msgseq
Enabled plugins
A concrete plugin-assisted scenario that exposed the issue was a LangBot plugin using tool / skill assisted output. The outbound QQ delivery is handled by LangBot's QQ Official adapter, and the issue appears to be in adapter-level message sequencing rather than plugin business logic.
Runtime environment
LangBot v4.10.x / latest, Docker deployment, Community Edition, QQ Official Bot API (
qqofficial).Observed in a Docker runtime with the QQ Official adapter while handling Agent / Skill / streaming / multi-part reply output.
Deployment version
Community Edition
Exception
When LangBot sends multiple replies or stream chunks for the same inbound QQ Official message, QQ may reject subsequent sends with duplicate-message detection:
Expected behavior:
For multiple outbound messages or stream chunks associated with the same inbound QQ
msg_id, LangBot should allocate a freshmsg_seqfor every send attempt.This should apply to normal text replies, mixed message chains that are split into multiple sends, rich media sends, streaming reply chunks, and retry attempts after failed sends.
Actual behavior:
In multi-part reply scenarios, the QQ Official adapter may reuse
msg_seq=1or omit proper per-replymsg_seqallocation. As a result, QQ Official API treats later sends as duplicate messages and returns:Suspected code locations:
Possible related areas:
send_private_text_msgsend_group_text_msgsend_channle_group_text_msgsend_channle_private_text_msg_send_media_msgsend_stream_msgreply_message_chunkIn the current behavior, stream replies appear especially likely to trigger the issue because
msg_seqcan remain fixed whileindexchanges.Suggested fix direction:
Introduce a QQ Official reply sequence allocator keyed by the inbound source
msg_id.Then every outbound send associated with an inbound
msg_idshould call this allocator:For stream replies,
indexcan continue to represent the stream chunk index, butmsg_seqshould also advance for each HTTP request:Why this fix is needed:
QQ Official API uses
msg_seqfor duplicate-message detection. If multiple sends under the same sourcemsg_idreuse the samemsg_seq, later messages can be rejected as duplicates.A per-source-message
msg_seqallocator would make QQ Official delivery safer for streaming, Agent, Skill, rich media, and other multi-part reply scenarios.Reproduction steps
Enabled plugins
A concrete plugin-assisted scenario that exposed the issue was a LangBot plugin using tool / skill assisted output. The outbound QQ delivery is handled by LangBot's QQ Official adapter, and the issue appears to be in adapter-level message sequencing rather than plugin business logic.