Merge pull request #2978 from AaronZ345/agent/feishu-message-recall

feat(feishu): cancel recalled messages
This commit is contained in:
zhayujie
2026-07-20 11:35:13 +08:00
committed by GitHub
4 changed files with 221 additions and 3 deletions

View File

@@ -562,6 +562,42 @@ class ChatChannel(Channel):
semaphore.release()
time.sleep(0.2)
def cancel_message(self, session_id: str, message_id: str):
"""Cancel one channel message without disturbing later queued work.
Queued contexts are matched by their original channel message ID. An
in-flight agent run is cancelled through the per-request token that the
channel placed on the context before dispatch.
"""
removed = 0
with self.lock:
session = self.sessions.get(session_id)
if session is not None:
context_queue = session[0]
kept = []
for _ in range(context_queue.qsize()):
context = context_queue.get_nowait()
context_queue.task_done()
message = context.get("msg") if context is not None else None
if getattr(message, "msg_id", None) == message_id:
removed += 1
else:
kept.append(context)
for context in kept:
context_queue.put(context)
from agent.protocol import get_cancel_registry
active = get_cancel_registry().cancel_request(message_id)
logger.info(
"[chat_channel] message recall: session=%s, message=%s, queued=%s, active=%s",
session_id,
message_id,
removed,
active,
)
return removed, active
# 取消session_id对应的所有任务只能取消排队的消息和已提交线程池但未执行的任务
def cancel_session(self, session_id):
with self.lock:

View File

@@ -63,7 +63,7 @@ python3 app.py
2. 进入应用详情 -> 事件订阅
3. 选择 **将事件发送至开发者服务器**
4. 填写请求地址: `http://your-domain:9891/`
5. 添加事件: `im.message.receive_v1` (接收消息v2.0)
5. 添加事件: `im.message.receive_v1` (接收消息v2.0)`im.message.recalled_v1` (消息撤回)
6. 保存配置
### 4. 注意事项
@@ -101,7 +101,7 @@ python3 app.py
1. 登录[飞书开放平台](https://open.feishu.cn/)
2. 进入应用详情 -> 事件订阅
3. 选择 **使用长连接接收事件**
4. 添加事件: `im.message.receive_v1` (接收消息v2.0)
4. 添加事件: `im.message.receive_v1` (接收消息v2.0)`im.message.recalled_v1` (消息撤回)
5. 保存配置
### 5. 注意事项
@@ -168,7 +168,7 @@ Address already in use
### 收不到消息
1. 检查飞书应用的事件订阅配置
2. 确认已添加 `im.message.receive_v1` 事件
2. 确认已添加 `im.message.receive_v1``im.message.recalled_v1` 事件
3. 检查应用权限: 需要 `im:message` 权限
4. 查看日志中的错误信息

View File

@@ -251,6 +251,8 @@ class FeiShuChanel(ChatChannel):
super().__init__()
# 历史消息id暂存用于幂等控制
self.receivedMsgs = ExpiredDict(60 * 60 * 7.1)
# Route recall events back to the session that accepted the message.
self._message_sessions = ExpiredDict(60 * 60 * 7.1)
self._http_server = None
self._ws_client = None
self._ws_thread = None
@@ -387,6 +389,19 @@ class FeiShuChanel(ChatChannel):
except Exception as e:
logger.error(f"[FeiShu] websocket handle message error: {e}", exc_info=True)
def handle_message_recalled_event(
data: lark.im.v1.P2ImMessageRecalledV1,
) -> None:
"""Cancel only the task created by the recalled Feishu message."""
try:
event_dict = json.loads(lark.JSON.marshal(data))
self._handle_message_recalled_event(event_dict.get("event", {}))
except Exception as e:
logger.error(
f"[FeiShu] websocket handle message recall error: {e}",
exc_info=True,
)
def handle_card_action(data):
"""Handle Card 2.0 button callbacks and update the card in place."""
try:
@@ -403,6 +418,7 @@ class FeiShuChanel(ChatChannel):
event_handler = (
lark.EventDispatcherHandler.builder("", "")
.register_p2_im_message_receive_v1(handle_message_event)
.register_p2_im_message_recalled_v1(handle_message_recalled_event)
.register_p2_card_action_trigger(handle_card_action)
.build()
)
@@ -591,6 +607,29 @@ class FeiShuChanel(ChatChannel):
)
return response
def _handle_message_recalled_event(self, event: dict):
"""Cancel one recalled message while preserving later queued messages."""
message_id = event.get("message_id")
if not message_id:
logger.warning(f"[FeiShu] invalid message recall event: {event}")
return 0, False
session_id = self._message_sessions.get(message_id)
if not session_id:
logger.info(
f"[FeiShu] ignored recall for unknown message, message_id={message_id}"
)
return 0, False
result = self.cancel_message(session_id, message_id)
self._message_sessions.pop(message_id, None)
logger.info(
"[FeiShu] recalled message cancelled, "
f"message_id={message_id}, session_id={session_id}, "
f"queued={result[0]}, active={result[1]}"
)
return result
def _handle_message_event(self, event: dict):
"""
处理消息事件的核心逻辑
@@ -724,6 +763,10 @@ class FeiShuChanel(ChatChannel):
no_need_at=True
)
if context:
# Feishu recall events only include message_id/chat_id. Keep the
# accepted route and use message_id as the agent cancellation key.
context["request_id"] = msg_id
self._message_sessions[msg_id] = context["session_id"]
# 流式回复模式:向 context 注入 on_event 回调agent 每产出一段文字时会调用它。
# 回调内部先发送一条占位消息获取 message_id之后通过 PATCH 接口原地更新内容,
# 实现打字机效果。回调结束时设置 context["feishu_streamed"]=True
@@ -2095,6 +2138,7 @@ class FeishuController:
FAILED_MSG = '{"success": false}'
SUCCESS_MSG = '{"success": true}'
MESSAGE_RECEIVE_TYPE = "im.message.receive_v1"
MESSAGE_RECALLED_TYPE = "im.message.recalled_v1"
CARD_ACTION_TYPE = "card.action.trigger"
def GET(self):
@@ -2134,6 +2178,8 @@ class FeishuController:
# 3. Handle message events.
if event_type == self.MESSAGE_RECEIVE_TYPE and event:
channel._handle_message_event(event)
elif event_type == self.MESSAGE_RECALLED_TYPE and event:
channel._handle_message_recalled_event(event)
return self.SUCCESS_MSG