feat(weixin): show typing indicator during message processing

This commit is contained in:
Hyu
2026-10-10 22:50:51 +08:00
parent 941ef00840
commit 5c8275e564
6 changed files with 318 additions and 12 deletions
+4 -1
View File
@@ -447,7 +447,10 @@ class RuntimePipeline:
lane_outcome = 'success'
lane_error = ''
try:
return await self._process_query(query)
from ..platform.processing_indicator import processing_indicator
async with processing_indicator(query.adapter, query.launcher_type.value.lower(), query.launcher_id):
return await self._process_query(query)
except asyncio.CancelledError:
lane_outcome = 'cancelled'
lane_error = 'cancelled'
+17 -11
View File
@@ -1272,17 +1272,23 @@ class RuntimeBot:
try:
with trace_mod.scope(route_ref=self._route_ref(event_binding, target_type, target_uuid)):
async for _ in self.ap.agent_run_orchestrator.run(
envelope,
binding,
adapter_context={
'_delivery_adapter': adapter,
'_platform_event': event,
'_execution_context': self.execution_context,
},
):
# Results are journaled by the orchestrator; platform sends require explicit actions.
pass
from .processing_indicator import processing_indicator
reply_type, reply_id = (None, None)
if isinstance(event, platform_events.MessageReceivedEvent):
reply_type, reply_id, _ = self._infer_reply_target(event)
async with processing_indicator(adapter, reply_type, reply_id):
async for _ in self.ap.agent_run_orchestrator.run(
envelope,
binding,
adapter_context={
'_delivery_adapter': adapter,
'_platform_event': event,
'_execution_context': self.execution_context,
},
):
# Results are journaled by the orchestrator; platform sends require explicit actions.
pass
except Exception:
return await self._record_event_route_trace(
event_type=event_type,
@@ -0,0 +1,13 @@
"""Optional platform processing status around actual processor execution."""
import contextlib
@contextlib.asynccontextmanager
async def processing_indicator(adapter, target_type, target_id):
factory = getattr(type(adapter), 'processing_indicator', None)
if callable(factory) and target_type and target_id is not None:
async with factory(adapter, target_type, str(target_id)):
yield
else:
yield
@@ -10,6 +10,7 @@ from __future__ import annotations
import asyncio
import base64
import contextlib
import os
import traceback
import typing
@@ -256,6 +257,8 @@ class OpenClawWeixinAdapter(abstract_platform_adapter.AbstractMessagePlatformAda
# context_token cache: from_user_id -> context_token
_context_tokens: dict[str, str] = pydantic.PrivateAttr(default_factory=dict)
_typing_indicator: typing.Any = pydantic.PrivateAttr(default=None)
_polling: bool = pydantic.PrivateAttr(default=False)
_poll_task: typing.Optional[asyncio.Task] = pydantic.PrivateAttr(default=None)
_bot_uuid: typing.Optional[str] = pydantic.PrivateAttr(default=None)
@@ -347,6 +350,18 @@ class OpenClawWeixinAdapter(abstract_platform_adapter.AbstractMessagePlatformAda
# Persist token to database so it survives restart
await self._persist_config()
@contextlib.asynccontextmanager
async def processing_indicator(self, target_type, target_id):
if target_type not in ('person', 'friend'):
yield
return
from langbot.libs.openclaw_weixin_api.typing_indicator import TypingIndicator
if self._typing_indicator is None:
self._typing_indicator = TypingIndicator(self.client, lambda peer: self._context_tokens.get(peer, ''))
async with self._typing_indicator.processing(target_id):
yield
async def send_message(
self,
target_type: str,
@@ -479,6 +494,9 @@ class OpenClawWeixinAdapter(abstract_platform_adapter.AbstractMessagePlatformAda
token=self.config.get('token', token),
)
self.bot_account_id = self.config.get('account_id', 'openclaw-weixin')
from langbot.libs.openclaw_weixin_api.typing_indicator import TypingIndicator
self._typing_indicator = TypingIndicator(self.client, lambda peer: self._context_tokens.get(peer, ''))
self._polling = True
# Start the long-poll loop
@@ -489,6 +507,9 @@ class OpenClawWeixinAdapter(abstract_platform_adapter.AbstractMessagePlatformAda
await self._poll_task
except asyncio.CancelledError:
pass
finally:
if self._typing_indicator is not None:
await self._typing_indicator.close()
async def _poll_loop(self):
"""Long-poll loop: call getUpdates continuously.
@@ -523,6 +544,9 @@ class OpenClawWeixinAdapter(abstract_platform_adapter.AbstractMessagePlatformAda
await self.logger.error('OpenClaw WeChat session expired, attempting re-login...')
try:
await self._do_login()
if self._typing_indicator is not None:
await self._typing_indicator.close()
self._typing_indicator = None
# Rebuild client with new credentials
self.client = OpenClawWeixinClient(
base_url=self.config.get('base_url', DEFAULT_BASE_URL),
@@ -616,6 +640,8 @@ class OpenClawWeixinAdapter(abstract_platform_adapter.AbstractMessagePlatformAda
async def kill(self) -> bool:
"""Stop the adapter."""
self._polling = False
if self._typing_indicator is not None:
await self._typing_indicator.close()
if self._poll_task and not self._poll_task.done():
self._poll_task.cancel()
try: