feat: auto-remove reaction after message processing complete

- _add_reaction now returns reaction_id on success
- Add _remove_reaction_sync and _remove_reaction methods
- Remove reaction when stream ends to clear processing indicator
- Store reaction_id in metadata for later removal
This commit is contained in:
Jiajun Xie 2026-04-03 21:07:41 +08:00 committed by chengyongru
parent a6aa0b7932
commit a0979d17ee

View File

@ -417,7 +417,7 @@ class FeishuChannel(BaseChannel):
return True
return self._is_bot_mentioned(message)
def _add_reaction_sync(self, message_id: str, emoji_type: str) -> None:
def _add_reaction_sync(self, message_id: str, emoji_type: str) -> str | None:
"""Sync helper for adding reaction (runs in thread pool)."""
from lark_oapi.api.im.v1 import CreateMessageReactionRequest, CreateMessageReactionRequestBody, Emoji
try:
@ -433,22 +433,54 @@ class FeishuChannel(BaseChannel):
if not response.success():
logger.warning("Failed to add reaction: code={}, msg={}", response.code, response.msg)
return None
else:
logger.debug("Added {} reaction to message {}", emoji_type, message_id)
return response.data.reaction_id if response.data else None
except Exception as e:
logger.warning("Error adding reaction: {}", e)
return None
async def _add_reaction(self, message_id: str, emoji_type: str = "THUMBSUP") -> None:
async def _add_reaction(self, message_id: str, emoji_type: str = "THUMBSUP") -> str | None:
"""
Add a reaction emoji to a message (non-blocking).
Common emoji types: THUMBSUP, OK, EYES, DONE, OnIt, HEART
"""
if not self._client:
return None
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, self._add_reaction_sync, message_id, emoji_type)
def _remove_reaction_sync(self, message_id: str, reaction_id: str) -> None:
"""Sync helper for removing reaction (runs in thread pool)."""
from lark_oapi.api.im.v1 import DeleteMessageReactionRequest
try:
request = DeleteMessageReactionRequest.builder() \
.message_id(message_id) \
.reaction_id(reaction_id) \
.build()
response = self._client.im.v1.message_reaction.delete(request)
if response.success():
logger.debug("Removed reaction {} from message {}", reaction_id, message_id)
else:
logger.debug("Failed to remove reaction: code={}, msg={}", response.code, response.msg)
except Exception as e:
logger.debug("Error removing reaction: {}", e)
async def _remove_reaction(self, message_id: str, reaction_id: str) -> None:
"""
Remove a reaction emoji from a message (non-blocking).
Used to clear the "processing" indicator after bot replies.
"""
if not self._client or not reaction_id:
return
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, self._add_reaction_sync, message_id, emoji_type)
await loop.run_in_executor(None, self._remove_reaction_sync, message_id, reaction_id)
# Regex to match markdown tables (header + separator + data rows)
_TABLE_RE = re.compile(
@ -1046,6 +1078,9 @@ class FeishuChannel(BaseChannel):
# --- stream end: final update or fallback ---
if meta.get("_stream_end"):
if (message_id := meta.get("message_id")) and (reaction_id := meta.get("reaction_id")):
await self._remove_reaction(message_id, reaction_id)
buf = self._stream_bufs.pop(chat_id, None)
if not buf or not buf.text:
return
@ -1227,7 +1262,7 @@ class FeishuChannel(BaseChannel):
return
# Add reaction
await self._add_reaction(message_id, self.config.react_emoji)
reaction_id = await self._add_reaction(message_id, self.config.react_emoji)
# Parse content
content_parts = []
@ -1305,6 +1340,7 @@ class FeishuChannel(BaseChannel):
media=media_paths,
metadata={
"message_id": message_id,
"reaction_id": reaction_id,
"chat_type": chat_type,
"msg_type": msg_type,
"parent_id": parent_id,