From 566669013f3f9c0b52cb1392250b76d510d99dc7 Mon Sep 17 00:00:00 2001 From: Cao Jiguang Date: Mon, 25 May 2026 12:06:49 +0800 Subject: [PATCH] fix(weixin): replace aiohttp ClientTimeout with asyncio.wait_for in _api_post/_api_get MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cron delivery to WeChat fails with 'Timeout context manager should be used inside a task' because _api_post and _api_get use aiohttp's ClientTimeout directly. When the cron scheduler calls send() via asyncio.run_coroutine_threadsafe(), aiohttp cannot find a running task and raises RuntimeError. _upload_media, _download_bytes, and _download_remote_media already use asyncio.wait_for() to avoid this. Apply the same pattern to _api_post and _api_get — the two remaining iLink API helpers that still use the raw ClientTimeout approach. This fixes cron delivery errors seen on the WeChat platform adapter when meyo-external cron jobs attempt to deliver output to WeChat. --- gateway/platforms/weixin.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/gateway/platforms/weixin.py b/gateway/platforms/weixin.py index 36bb3dd21c2c9..73e9e68ea70f6 100644 --- a/gateway/platforms/weixin.py +++ b/gateway/platforms/weixin.py @@ -378,12 +378,16 @@ async def _api_post( ) -> Dict[str, Any]: body = _json_dumps({**payload, "base_info": _base_info()}) url = f"{base_url.rstrip('/')}/{endpoint}" - timeout = aiohttp.ClientTimeout(total=timeout_ms / 1000) - async with session.post(url, data=body, headers=_headers(token, body), timeout=timeout) as response: - raw = await response.text() - if not response.ok: - raise RuntimeError(f"iLink POST {endpoint} HTTP {response.status}: {raw[:200]}") - return json.loads(raw) + # Use asyncio.wait_for() instead of aiohttp ClientTimeout to avoid + # "Timeout context manager should be used inside a task" errors when + # invoked via asyncio.run_coroutine_threadsafe() from cron jobs. + async def _do() -> Dict[str, Any]: + async with session.post(url, data=body, headers=_headers(token, body)) as response: + raw = await response.text() + if not response.ok: + raise RuntimeError(f"iLink POST {endpoint} HTTP {response.status}: {raw[:200]}") + return json.loads(raw) + return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) async def _api_get( @@ -398,12 +402,16 @@ async def _api_get( "iLink-App-Id": ILINK_APP_ID, "iLink-App-ClientVersion": str(ILINK_APP_CLIENT_VERSION), } - timeout = aiohttp.ClientTimeout(total=timeout_ms / 1000) - async with session.get(url, headers=headers, timeout=timeout) as response: - raw = await response.text() - if not response.ok: - raise RuntimeError(f"iLink GET {endpoint} HTTP {response.status}: {raw[:200]}") - return json.loads(raw) + # Use asyncio.wait_for() instead of aiohttp ClientTimeout to avoid + # "Timeout context manager should be used inside a task" errors when + # invoked via asyncio.run_coroutine_threadsafe() from cron jobs. + async def _do() -> Dict[str, Any]: + async with session.get(url, headers=headers) as response: + raw = await response.text() + if not response.ok: + raise RuntimeError(f"iLink GET {endpoint} HTTP {response.status}: {raw[:200]}") + return json.loads(raw) + return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) async def _get_updates(