fix(weixin): check both ret and errcode on send to avoid silent drops

The iLink API signals failures through either `ret` or `errcode`.
`_poll_once` already checked both, but `_send_text` and `_send_media_file`
only checked `errcode`. When the API returned `ret != 0` with
`errcode == 0`, the send appeared successful but the message was never
delivered, causing the "still losing messages" issue.

- Add `_check_response_error` helper that validates both fields
- Use it in `_send_text` and `_send_media_file`
- Add debug log after successful text send for observability
- Add test for nonzero ret with zero errcode

Refs: previous inbound fix (suppress -> explicit try/except)
This commit is contained in:
chengyongru 2026-05-07 16:20:08 +08:00
parent 2a318d6991
commit e9f4a868a8
2 changed files with 39 additions and 10 deletions

View File

@ -526,6 +526,22 @@ class WeixinChannel(BaseChannel):
f"WeChat session paused, {remaining_min} min remaining (errcode {ERRCODE_SESSION_EXPIRED})"
)
@staticmethod
def _check_response_error(data: dict, operation: str) -> None:
"""Check both ``ret`` and ``errcode`` like the reference TS code.
The iLink API may signal failure through either field (or both).
``_poll_once`` already checks both; outbound send helpers must do
the same to avoid silent drops.
"""
ret = data.get("ret", 0)
errcode = data.get("errcode", 0)
is_error = (ret is not None and ret != 0) or (errcode is not None and errcode != 0)
if is_error:
raise RuntimeError(
f"WeChat {operation} error (ret={ret}, errcode={errcode}): {data.get('errmsg', '')}"
)
async def _poll_once(self) -> None:
remaining = self._session_pause_remaining_s()
if remaining > 0:
@ -1123,11 +1139,8 @@ class WeixinChannel(BaseChannel):
}
data = await self._api_post("ilink/bot/sendmessage", body)
errcode = data.get("errcode", 0)
if errcode and errcode != 0:
raise RuntimeError(
f"WeChat send text error (code {errcode}): {data.get('errmsg', '')}"
)
self._check_response_error(data, "send text")
self.logger.debug("WeChat text sent to {} (client_id={})", to_user_id, client_id)
async def _send_media_file(
self,
@ -1273,11 +1286,7 @@ class WeixinChannel(BaseChannel):
}
data = await self._api_post("ilink/bot/sendmessage", body)
errcode = data.get("errcode", 0)
if errcode and errcode != 0:
raise RuntimeError(
f"WeChat send media error (code {errcode}): {data.get('errmsg', '')}"
)
self._check_response_error(data, "send media")
# ---------------------------------------------------------------------------

View File

@ -1252,6 +1252,26 @@ async def test_send_text_succeeds_on_zero_errcode() -> None:
channel._api_post.assert_awaited_once()
@pytest.mark.asyncio
async def test_send_text_raises_on_nonzero_ret_even_when_errcode_zero() -> None:
"""_send_text must raise when the API returns ret != 0, even if errcode is 0.
The iLink API signals failure through either field. Checking only errcode
caused silent message drops (responses generated but never delivered).
"""
channel, _bus = _make_channel()
channel._client = object()
channel._token = "token"
channel._api_post = AsyncMock(
return_value={"ret": -100, "errcode": 0, "errmsg": "internal error"}
)
with pytest.raises(RuntimeError, match="WeChat send text error.*ret=-100.*errcode=0"):
await channel._send_text("wx-user", "hello", "ctx-ok")
channel._api_post.assert_awaited_once()
# ---------------------------------------------------------------------------
# Tests for _poll_once not silently dropping messages on processing errors
# ---------------------------------------------------------------------------