mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-19 16:12:30 +00:00
fix(weixin): treat ret=-2 as non-fatal on sendmessage and align client_id format
The iLink sendmessage API frequently returns ret=-2 (parameter error / rate
limit / expired token) even when HTTP status is 200. The openclaw reference
plugin ignores the JSON body for sendmessage entirely and only checks HTTP
status. Our previous strict ret checking turned ret=-2 into RuntimeError,
causing ChannelManager retries which only made things worse.
Changes:
- _send_text: swallow ret=-2 after one retry without context_token.
Log request body + response at warning level for diagnostics.
- _send_media_file: same ret=-2 swallowing.
- _generate_client_id: change format to ``nanobot:{timestamp}-{hex}`` to
match openclaw-weixin ``{prefix}:{Date.now()}-{hex}``.
- Update tests to expect swallowing instead of raising for ret=-2.
This commit is contained in:
parent
28358980ed
commit
9fefb31344
@ -18,7 +18,6 @@ import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
@ -1109,6 +1108,14 @@ class WeixinChannel(BaseChannel):
|
||||
except Exception as e:
|
||||
self.logger.debug("typing clear failed for {}: {}", chat_id, e)
|
||||
|
||||
@staticmethod
|
||||
def _generate_client_id() -> str:
|
||||
"""Generate a client_id matching the reference plugin format.
|
||||
|
||||
openclaw-weixin uses ``{prefix}:{timestamp}-{8-char hex}``.
|
||||
"""
|
||||
return f"nanobot:{int(time.time() * 1000)}-{os.urandom(4).hex()}"
|
||||
|
||||
async def _send_text(
|
||||
self,
|
||||
to_user_id: str,
|
||||
@ -1116,7 +1123,7 @@ class WeixinChannel(BaseChannel):
|
||||
context_token: str,
|
||||
) -> None:
|
||||
"""Send a text message matching the exact protocol from send.ts."""
|
||||
client_id = f"nanobot-{uuid.uuid4().hex[:12]}"
|
||||
client_id = self._generate_client_id()
|
||||
|
||||
item_list: list[dict] = []
|
||||
if text:
|
||||
@ -1143,11 +1150,14 @@ class WeixinChannel(BaseChannel):
|
||||
ret = data.get("ret", 0)
|
||||
errcode = data.get("errcode", 0)
|
||||
|
||||
# If ret=-2 (parameter error / rate limit / expired token) and we sent
|
||||
# with a context_token, retry once without it. The openclaw reference
|
||||
# plugin can send without a token (it just warns), and issue #61174
|
||||
# shows that expired context_tokens are a common cause of ret=-2.
|
||||
if ret == -2 and context_token:
|
||||
# The iLink sendmessage API frequently returns ret=-2 (parameter
|
||||
# error / rate limit / expired token) even though HTTP status is 200.
|
||||
# The openclaw reference plugin ignores the JSON body for sendmessage
|
||||
# and only checks HTTP status. We retry once without context_token
|
||||
# (a common fix for token expiry per openclaw#61174), but if that
|
||||
# also fails we swallow ret=-2 to avoid silent message drops.
|
||||
if ret == -2:
|
||||
if context_token:
|
||||
self.logger.warning(
|
||||
"WeChat send text returned ret=-2 for {} (client_id={}); "
|
||||
"retrying without context_token",
|
||||
@ -1167,6 +1177,19 @@ class WeixinChannel(BaseChannel):
|
||||
)
|
||||
self._context_tokens.pop(to_user_id, None)
|
||||
self._save_state()
|
||||
self.logger.debug(
|
||||
"WeChat text sent to {} (client_id={})", to_user_id, client_id
|
||||
)
|
||||
return
|
||||
# Treat persistent ret=-2 as non-fatal (matching reference plugin).
|
||||
self.logger.warning(
|
||||
"WeChat send text returned ret=-2 for {} (client_id={}); "
|
||||
"treating as non-fatal. Request body: {}. Response: {}",
|
||||
to_user_id,
|
||||
client_id,
|
||||
json.dumps(body, ensure_ascii=False, default=str),
|
||||
json.dumps(data, ensure_ascii=False, default=str),
|
||||
)
|
||||
self.logger.debug("WeChat text sent to {} (client_id={})", to_user_id, client_id)
|
||||
return
|
||||
|
||||
@ -1297,7 +1320,7 @@ class WeixinChannel(BaseChannel):
|
||||
media_item["len"] = str(raw_size)
|
||||
|
||||
# Send each media item as its own message (matching reference plugin)
|
||||
client_id = f"nanobot-{uuid.uuid4().hex[:12]}"
|
||||
client_id = self._generate_client_id()
|
||||
item_list: list[dict] = [{"type": item_type, item_key: media_item}]
|
||||
|
||||
weixin_msg: dict[str, Any] = {
|
||||
@ -1317,6 +1340,15 @@ class WeixinChannel(BaseChannel):
|
||||
}
|
||||
|
||||
data = await self._api_post("ilink/bot/sendmessage", body)
|
||||
ret = data.get("ret", 0)
|
||||
if ret == -2:
|
||||
# See _send_text for rationale: openclaw ignores ret on sendmessage.
|
||||
self.logger.warning(
|
||||
"WeChat send media returned ret=-2 for {} (client_id={}); treating as non-fatal",
|
||||
to_user_id,
|
||||
client_id,
|
||||
)
|
||||
return
|
||||
self._check_response_error(data, "send media", body=body)
|
||||
|
||||
|
||||
|
||||
@ -1394,8 +1394,8 @@ async def test_send_text_retries_without_context_token_on_ret_minus_two() -> Non
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_text_raises_when_ret_minus_two_retry_also_fails() -> None:
|
||||
"""If both attempts (with and without token) return ret=-2, raise."""
|
||||
async def test_send_text_swallows_ret_minus_two_when_retry_also_fails() -> None:
|
||||
"""If both attempts return ret=-2, swallow the error (matching openclaw)."""
|
||||
channel, _bus = _make_channel()
|
||||
channel._client = object()
|
||||
channel._token = "token"
|
||||
@ -1408,7 +1408,7 @@ async def test_send_text_raises_when_ret_minus_two_retry_also_fails() -> None:
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="ret=-2"):
|
||||
# Should NOT raise
|
||||
await channel._send_text("wx-user", "hello", "bad-token")
|
||||
|
||||
assert channel._api_post.await_count == 2
|
||||
@ -1417,15 +1417,15 @@ async def test_send_text_raises_when_ret_minus_two_retry_also_fails() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_text_does_not_retry_without_token_when_no_context_token() -> None:
|
||||
"""If no context_token was provided, ret=-2 should raise immediately."""
|
||||
async def test_send_text_swallows_ret_minus_two_when_no_context_token() -> None:
|
||||
"""If no context_token was provided, ret=-2 is swallowed without retry."""
|
||||
channel, _bus = _make_channel()
|
||||
channel._client = object()
|
||||
channel._token = "token"
|
||||
|
||||
channel._api_post = AsyncMock(return_value={"ret": -2})
|
||||
|
||||
with pytest.raises(RuntimeError, match="ret=-2"):
|
||||
# Should NOT raise
|
||||
await channel._send_text("wx-user", "hello", "")
|
||||
|
||||
# Only one API call (no retry)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user